import { Hype } from '@because/hype'
import { define, stylesToCSS } from '@because/forge'
import { baseStyles, ToolScript, theme } from '@because/toes/tools'
import { readdir, stat } from 'fs/promises'
import { readFileSync } from 'fs'
import { join, resolve, extname, basename } from 'path'
import type { Child } from 'hono/jsx'

const APPS_DIR = process.env.APPS_DIR!

const safePath = (base: string, ...segments: string[]) => {
  const norm = resolve(base)
  const full = resolve(norm, ...segments)
  if (!full.startsWith(norm + '/') && full !== norm) return null
  return full
}

const app = new Hype({ prettyHTML: false })

const Container = define('Container', {
  fontFamily: theme('fonts-sans'),
  padding: '20px',
  paddingTop: 0,
  maxWidth: '1200px',
  margin: '0 auto',
  color: theme('colors-text'),
})

const FileList = define('FileList', {
  listStyle: 'none',
  padding: 0,
  margin: '20px 0',
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  overflow: 'hidden',
})

const FileItem = define('FileItem', {
  padding: '10px 15px',
  borderBottom: `1px solid ${theme('colors-border')}`,
  states: {
    ':last-child': {
      borderBottom: 'none',
    },
    ':hover': {
      backgroundColor: theme('colors-bgHover'),
    },
  }
})

const FileLink = define('FileLink', {
  base: 'a',
  textDecoration: 'none',
  color: theme('colors-link'),
  display: 'flex',
  alignItems: 'center',
  gap: '8px',
  states: {
    ':hover': {
      textDecoration: 'underline',
    },
  }
})

const FileIcon = define('FileIcon', {
  base: 'svg',
  width: '18px',
  height: '18px',
  flexShrink: 0,
  fill: theme('colors-textMuted'),
})

const CodeBlock = define('CodeBlock', {
  margin: '20px 0',
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  overflowX: 'auto',
  selectors: {
    '& pre': {
      margin: 0,
      padding: '15px',
      whiteSpace: 'pre',
      backgroundColor: theme('colors-bgSubtle'),
    },
    '& pre code': {
      whiteSpace: 'pre',
      fontFamily: theme('fonts-mono'),
    },
  },
})

const CodeHeader = define('CodeHeader', {
  padding: '10px 15px',
  backgroundColor: theme('colors-bgElement'),
  borderBottom: `1px solid ${theme('colors-border')}`,
  fontWeight: 'bold',
  fontSize: '14px',
  display: 'flex',
  justifyContent: 'space-between',
  alignItems: 'center',
})

const ErrorBox = define('ErrorBox', {
  color: theme('colors-error'),
  padding: '20px',
  backgroundColor: theme('colors-bgElement'),
  borderRadius: theme('radius-md'),
  margin: '20px 0',
})

const Breadcrumb = define('Breadcrumb', {
  display: 'flex',
  alignItems: 'center',
  gap: '6px',
  fontSize: '14px',
  marginBottom: '15px',
  flexWrap: 'wrap',
})

const BreadcrumbLink = define('BreadcrumbLink', {
  base: 'a',
  textDecoration: 'none',
  color: theme('colors-link'),
  states: {
    ':hover': {
      textDecoration: 'underline',
    },
  },
})

const BreadcrumbSeparator = define('BreadcrumbSeparator', {
  color: theme('colors-textMuted'),
})

const BreadcrumbCurrent = define('BreadcrumbCurrent', {
  color: theme('colors-text'),
  fontWeight: 500,
})

const MediaContainer = define('MediaContainer', {
  margin: '20px 0',
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  overflow: 'hidden',
  backgroundColor: theme('colors-bgSubtle'),
})

const MediaHeader = define('MediaHeader', {
  padding: '10px 15px',
  backgroundColor: theme('colors-bgElement'),
  borderBottom: `1px solid ${theme('colors-border')}`,
  fontWeight: 'bold',
  fontSize: '14px',
  display: 'flex',
  justifyContent: 'space-between',
  alignItems: 'center',
})

const MediaContent = define('MediaContent', {
  padding: '20px',
  display: 'flex',
  justifyContent: 'center',
  alignItems: 'center',
})

const ImagePreview = define('ImagePreview', {
  base: 'img',
  maxWidth: '100%',
  maxHeight: '600px',
  objectFit: 'contain',
})

const AudioPlayer = define('AudioPlayer', {
  base: 'audio',
  width: '100%',
  maxWidth: '500px',
})

const VideoPlayer = define('VideoPlayer', {
  base: 'video',
  maxWidth: '100%',
  maxHeight: '600px',
})

const DownloadButton = define('DownloadButton', {
  base: 'a',
  display: 'inline-flex',
  alignItems: 'center',
  gap: '6px',
  padding: '8px 16px',
  backgroundColor: theme('colors-primary'),
  color: 'white',
  textDecoration: 'none',
  borderRadius: theme('radius-md'),
  fontSize: '14px',
  states: {
    ':hover': {
      opacity: 0.9,
    },
  },
})

const EditButton = define('EditButton', {
  base: 'button',
  display: 'inline-flex',
  alignItems: 'center',
  gap: '6px',
  padding: '6px 12px',
  backgroundColor: theme('colors-bgElement'),
  color: theme('colors-text'),
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  fontSize: '13px',
  cursor: 'pointer',
  states: {
    ':hover': {
      backgroundColor: theme('colors-bgHover'),
    },
  },
})

const EditLink = define('EditLink', {
  base: 'a',
  display: 'inline-flex',
  alignItems: 'center',
  gap: '6px',
  padding: '6px 12px',
  backgroundColor: theme('colors-bgElement'),
  color: theme('colors-text'),
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
  fontSize: '13px',
  cursor: 'pointer',
  textDecoration: 'none',
  states: {
    ':hover': {
      backgroundColor: theme('colors-bgHover'),
    },
  },
})


const FolderIcon = () => (
  <FileIcon viewBox="0 0 24 24">
    <path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
  </FileIcon>
)

const FileIconSvg = () => (
  <FileIcon viewBox="0 0 24 24">
    <path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 2l5 5h-5V4zM6 20V4h6v6h6v10H6z" />
  </FileIcon>
)

interface LayoutProps {
  title: string
  children: Child
  highlight?: boolean
  editable?: boolean
}

const fileMemoryScript = `
(function() {
  var params = new URLSearchParams(window.location.search);
  var app = params.get('app');
  var file = params.get('file');
  if (!app) return;
  var key = 'code-app:' + app + ':file';
  if (params.has('file')) {
    if (file) localStorage.setItem(key, file);
    else localStorage.removeItem(key);
  } else {
    var saved = localStorage.getItem(key);
    if (saved) {
      window.location.replace('/?app=' + encodeURIComponent(app) + '&file=' + encodeURIComponent(saved));
    }
  }
})();
`

function Layout({ title, children, highlight, editable }: LayoutProps) {
  return (
    <html>
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>{title}</title>
        <link rel="stylesheet" href="/styles.css" />
        {highlight && !editable && (
          <>
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css" media="(prefers-color-scheme: light)" />
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" media="(prefers-color-scheme: dark)" />
            <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
          </>
        )}
        {editable && (
          <>
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" media="(prefers-color-scheme: light)" />
            <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" media="(prefers-color-scheme: dark)" />
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-tsx.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
            <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markdown.min.js"></script>
                      </>
        )}
      </head>
      <body>
        <script dangerouslySetInnerHTML={{ __html: fileMemoryScript }} />
        <ToolScript />
        <Container>
          {children}
        </Container>
        {highlight && !editable && <script dangerouslySetInnerHTML={{ __html: 'hljs.highlightAll();' }} />}
      </body>
    </html>
  )
}

app.get('/ok', c => c.text('ok'))

app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, {
  'Content-Type': 'text/css; charset=utf-8',
}))

app.get('/raw', async c => {
  const appName = c.req.query('app')
  const filePath = c.req.query('file')

  if (!appName || !filePath) {
    return c.text('Missing app or file parameter', 400)
  }

  const fullPath = safePath(APPS_DIR, appName, filePath)
  if (!fullPath) return c.text('Invalid path', 400)
  const file = Bun.file(fullPath)

  if (!await file.exists()) {
    return c.text(`File not found: ${fullPath}`, 404)
  }

  return new Response(file)
})

app.post('/save', async c => {
  const appName = c.req.query('app')
  const filePath = c.req.query('file')

  if (!appName || !filePath) {
    return c.text('Missing app or file parameter', 400)
  }

  const fullPath = safePath(APPS_DIR, appName, filePath)
  if (!fullPath) return c.text('Invalid path', 400)
  const content = await c.req.text()

  try {
    await Bun.write(fullPath, content)
    return c.text('OK')
  } catch (err) {
    return c.text(`Failed to save: ${err}`, 500)
  }
})

async function listFiles(appPath: string, subPath: string = '') {
  const fullPath = join(appPath, subPath)
  const entries = await readdir(fullPath, { withFileTypes: true })

  const items = entries.map(entry => ({
    name: entry.name,
    isDirectory: entry.isDirectory(),
    path: subPath ? `${subPath}/${entry.name}` : entry.name,
  }))

  return items.sort((a, b) => {
    if (a.isDirectory !== b.isDirectory) {
      return a.isDirectory ? -1 : 1
    }
    return a.name.localeCompare(b.name)
  })
}

interface BreadcrumbProps {
  appName: string
  filePath: string
}

function PathBreadcrumb({ appName, filePath }: BreadcrumbProps) {
  const parts = filePath ? filePath.split('/').filter(Boolean) : []
  const crumbs: { name: string; path: string }[] = []

  let currentPath = ''
  for (const part of parts) {
    currentPath = currentPath ? `${currentPath}/${part}` : part
    crumbs.push({ name: part, path: currentPath })
  }

  return (
    <Breadcrumb>
      {crumbs.length > 0 ? (
        <BreadcrumbLink href={`/?app=${appName}&file=`}>{appName}</BreadcrumbLink>
      ) : (
        <BreadcrumbCurrent>{appName}</BreadcrumbCurrent>
      )}
      {crumbs.map((crumb, i) => (
        <>
          <BreadcrumbSeparator>/</BreadcrumbSeparator>
          {i === crumbs.length - 1 ? (
            <BreadcrumbCurrent>{crumb.name}</BreadcrumbCurrent>
          ) : (
            <BreadcrumbLink href={`/?app=${appName}&file=${crumb.path}`}>{crumb.name}</BreadcrumbLink>
          )}
        </>
      ))}
    </Breadcrumb>
  )
}

const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp'])
const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac'])
const VIDEO_EXTS = new Set(['.mp4', '.webm', '.mov', '.avi'])
const BINARY_EXTS = new Set(['.pdf', '.zip', '.tar', '.gz', '.exe', '.dmg', '.woff', '.woff2', '.ttf', '.otf', '.eot'])

type FileType = 'text' | 'image' | 'audio' | 'video' | 'binary'

function getFileType(filename: string): FileType {
  const ext = extname(filename).toLowerCase()
  if (IMAGE_EXTS.has(ext)) return 'image'
  if (AUDIO_EXTS.has(ext)) return 'audio'
  if (VIDEO_EXTS.has(ext)) return 'video'
  if (BINARY_EXTS.has(ext)) return 'binary'
  return 'text'
}

function getLanguage(filename: string): string {
  const ext = extname(filename).toLowerCase()
  const langMap: Record<string, string> = {
    '.js': 'javascript',
    '.jsx': 'javascript',
    '.ts': 'typescript',
    '.tsx': 'typescript',
    '.json': 'json',
    '.css': 'css',
    '.html': 'html',
    '.md': 'markdown',
    '.sh': 'bash',
    '.yml': 'yaml',
    '.yaml': 'yaml',
  }
  return langMap[ext] || 'plaintext'
}

function getPrismLanguage(filename: string): string {
  const ext = extname(filename).toLowerCase()
  const langMap: Record<string, string> = {
    '.js': 'javascript',
    '.jsx': 'javascript',
    '.ts': 'typescript',
    '.tsx': 'typescript',
    '.json': 'json',
    '.css': 'css',
    '.html': 'html',
    '.md': 'markdown',
    '.sh': 'bash',
    '.yml': 'yaml',
    '.yaml': 'yaml',
    '.py': 'python',
    '.rb': 'ruby',
    '.go': 'go',
    '.rs': 'rust',
    '.sql': 'sql',
  }
  return langMap[ext] || 'plaintext'
}


app.get('/', async c => {
  const appName = c.req.query('app')
  const filePath = c.req.query('file') || ''

  if (!appName) {
    return c.html(
      <Layout title="Code Browser">
        <ErrorBox>Please specify an app name with ?app=&lt;name&gt;</ErrorBox>
      </Layout>
    )
  }

  const appPath = safePath(APPS_DIR, appName)
  if (!appPath) {
    return c.html(
      <Layout title="Code Browser">
        <ErrorBox>Invalid app name</ErrorBox>
      </Layout>
    )
  }

  try {
    await stat(appPath)
  } catch {
    return c.html(
      <Layout title="Code Browser">
        <ErrorBox>App "{appName}" not found</ErrorBox>
      </Layout>
    )
  }

  const fullPath = safePath(appPath, filePath)
  if (!fullPath) {
    return c.html(
      <Layout title="Code Browser">
        <ErrorBox>Invalid file path</ErrorBox>
      </Layout>
    )
  }

  let fileStats

  try {
    fileStats = await stat(fullPath)
  } catch {
    return c.html(
      <Layout title="Code Browser">
        <ErrorBox>Path "{filePath}" not found</ErrorBox>
      </Layout>
    )
  }

  if (fileStats.isFile()) {
    const filename = basename(fullPath)
    const fileType = getFileType(filename)
    const rawUrl = `/raw?app=${appName}&file=${filePath}`
    const downloadUrl = `${rawUrl}&download=1`

    if (fileType === 'image') {
      return c.html(
        <Layout title={`${appName}/${filePath}`}>
          <PathBreadcrumb appName={appName} filePath={filePath} />
          <MediaContainer>
            <MediaHeader>
              <span>{filename}</span>
              <DownloadButton href={downloadUrl}>Download</DownloadButton>
            </MediaHeader>
            <MediaContent>
              <ImagePreview src={rawUrl} alt={filename} />
            </MediaContent>
          </MediaContainer>
        </Layout>
      )
    }

    if (fileType === 'audio') {
      return c.html(
        <Layout title={`${appName}/${filePath}`}>
          <PathBreadcrumb appName={appName} filePath={filePath} />
          <MediaContainer>
            <MediaHeader>
              <span>{filename}</span>
              <DownloadButton href={downloadUrl}>Download</DownloadButton>
            </MediaHeader>
            <MediaContent>
              <AudioPlayer controls src={rawUrl} />
            </MediaContent>
          </MediaContainer>
        </Layout>
      )
    }

    if (fileType === 'video') {
      return c.html(
        <Layout title={`${appName}/${filePath}`}>
          <PathBreadcrumb appName={appName} filePath={filePath} />
          <MediaContainer>
            <MediaHeader>
              <span>{filename}</span>
              <DownloadButton href={downloadUrl}>Download</DownloadButton>
            </MediaHeader>
            <MediaContent>
              <VideoPlayer controls src={rawUrl} />
            </MediaContent>
          </MediaContainer>
        </Layout>
      )
    }

    if (fileType === 'binary') {
      return c.redirect(downloadUrl)
    }

    // Text file - show with syntax highlighting
    const content = readFileSync(fullPath, 'utf-8')
    const language = getLanguage(filename)
    const prismLang = getPrismLanguage(filename)
    const edit = c.req.query('edit') === '1'

    if (edit) {
      const editorScript = `
import { CodeJar } from 'https://cdn.jsdelivr.net/npm/codejar@4.2.0/dist/codejar.js';

const editor = document.getElementById('editor');
const saveBtn = document.getElementById('save-btn');
const status = document.getElementById('save-status');
let dirty = false;

const highlight = (el) => {
  Prism.highlightElement(el);
};

const jar = CodeJar(editor, highlight, { tab: '  ', addClosing: false });

// Initial highlight
highlight(editor);

jar.onUpdate(() => {
  if (!dirty) {
    dirty = true;
    saveBtn.textContent = 'Save *';
  }
});

saveBtn.onclick = async () => {
  if (!dirty) return;
  saveBtn.disabled = true;
  status.textContent = 'Saving...';

  try {
    const res = await fetch('/save?app=${appName}&file=${filePath}', {
      method: 'POST',
      headers: { 'Content-Type': 'text/plain' },
      body: jar.toString()
    });
    if (!res.ok) throw new Error('Save failed');
    dirty = false;
    saveBtn.textContent = 'Save';
    saveBtn.disabled = false;
    status.textContent = 'Saved!';
    setTimeout(() => { status.textContent = ''; }, 2000);
  } catch (err) {
    saveBtn.disabled = false;
    status.textContent = 'Error: ' + err.message;
  }
};

document.addEventListener('keydown', (e) => {
  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
    e.preventDefault();
    saveBtn.click();
  }
});
`
      return c.html(
        <Layout title={`${appName}/${filePath}`} editable>
          <PathBreadcrumb appName={appName} filePath={filePath} />
          <CodeBlock>
            <CodeHeader>
              <span>{filename}</span>
              <div style="display:flex;align-items:center;gap:8px">
                <span id="save-status" style="font-size:12px;font-weight:normal;color:var(--colors-textMuted)"></span>
                <EditButton id="save-btn">Save</EditButton>
                <EditLink href={`/?app=${appName}&file=${filePath}`}>Done</EditLink>
              </div>
            </CodeHeader>
            <pre id="editor" class={`language-${prismLang}`} contenteditable style="margin:0;padding:15px;min-height:300px;outline:none">{content}</pre>
          </CodeBlock>
          <script type="module" dangerouslySetInnerHTML={{ __html: editorScript }} />
        </Layout>
      )
    }

    return c.html(
      <Layout title={`${appName}/${filePath}`} highlight>
        <PathBreadcrumb appName={appName} filePath={filePath} />
        <CodeBlock>
          <CodeHeader>
            <span>{filename}</span>
            <EditLink href={`/?app=${appName}&file=${filePath}&edit=1`}>Edit</EditLink>
          </CodeHeader>
          <pre><code class={`language-${language}`}>{content}</code></pre>
        </CodeBlock>
      </Layout>
    )
  }

  const files = await listFiles(appPath, filePath)

  return c.html(
    <Layout title={`${appName}${filePath ? `/${filePath}` : ''}`}>
      <PathBreadcrumb appName={appName} filePath={filePath} />
      <FileList>
        {files.map(file => (
          <FileItem>
            <FileLink href={`/?app=${appName}&file=${file.path}`}>
              {file.isDirectory ? <FolderIcon /> : <FileIconSvg />}
              <span>{file.name}</span>
            </FileLink>
          </FileItem>
        ))}
      </FileList>
    </Layout>
  )
})

export default app.defaults
