#!/usr/bin/env python3
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from urllib.parse import unquote, quote
from pathlib import Path
from datetime import datetime
from string import Template
from typing import Optional
import html
import markdown
import mimetypes
import posixpath

APP_DIR = Path(__file__).resolve().parent
ROOT = Path('/local/usr/hermes-workdir').resolve()
TITLE = 'wanyue'
BASE_PREFIX = ''
STATIC_PREFIX = '/static/'
TEMPLATE_PATH = APP_DIR / 'templates' / 'index.html'
STATIC_DIR = APP_DIR / 'static'
MARKDOWN_EXTENSIONS = {'.md', '.markdown'}
CODE_EXTENSIONS = {
    '.py', '.js', '.ts', '.tsx', '.jsx', '.css', '.html', '.htm', '.sh', '.bash',
    '.zsh', '.yaml', '.yml', '.xml', '.json', '.toml', '.ini', '.cfg', '.conf',
    '.sql', '.svg', '.svgz'
}
PLAIN_TEXT_EXTENSIONS = {'.txt', '.csv', '.log'}
TEXT_EXTENSIONS = MARKDOWN_EXTENSIONS | CODE_EXTENSIONS | PLAIN_TEXT_EXTENSIONS


def fmt_size(size: int) -> str:
    if size < 1024:
        return f"{size} B"
    value = float(size)
    for unit in ["KB", "MB", "GB", "TB"]:
        value /= 1024.0
        if value < 1024 or unit == "TB":
            return f"{value:.1f} {unit}"



def safe_target(url_path: str) -> Path:
    rel = posixpath.normpath('/' + unquote(url_path)).lstrip('/')
    target = (ROOT / rel).resolve()
    if target != ROOT and ROOT not in target.parents:
        raise PermissionError
    return target



def normalize_request_path(request_path: str) -> str:
    path = request_path.split('?', 1)[0]
    if path == BASE_PREFIX:
        return '/'
    if path.startswith(BASE_PREFIX + '/'):
        trimmed = path[len(BASE_PREFIX):]
        return trimmed or '/'
    return path



def get_preview_kind(target: Path) -> Optional[str]:
    suffix = target.suffix.lower()
    mime = mimetypes.guess_type(str(target))[0] or ''
    if mime.startswith('image/') and suffix not in {'.svg', '.svgz'}:
        return 'image'
    if mime.startswith('video/'):
        return 'video'
    if mime.startswith('audio/'):
        return 'audio'
    if suffix == '.pdf':
        return 'pdf'
    if suffix in MARKDOWN_EXTENSIONS:
        return 'markdown'
    if suffix in CODE_EXTENSIONS:
        return 'code'
    if mime.startswith('text/') or suffix in PLAIN_TEXT_EXTENSIONS or suffix in TEXT_EXTENSIONS:
        return 'plain'
    return None



def get_icon(is_dir: bool, is_parent: bool = False) -> str:
    if is_parent:
        return '<span class="item-icon parent-icon" aria-hidden="true">↩</span>'
    if is_dir:
        return '<span class="item-icon folder-icon" aria-hidden="true">📁</span>'
    return '<span class="item-icon file-icon" aria-hidden="true">📄</span>'



def render_name_cell(name: str, sub: str, href: str, icon_html: str, extra_attrs: str = '') -> str:
    return (
        f'<a class="item-link" href="{href}"{extra_attrs}>'
        f'<span class="item-main">{icon_html}'
        f'<span class="item-text"><span class="item-name">{html.escape(name)}</span>'
        f'<span class="item-sub">{html.escape(sub)}</span></span></span>'
        '</a>'
    )



def render_preview_button(entry: dict) -> str:
    if not entry.get('preview_kind'):
        return ''
    meta = f"{entry['type']} · {entry['size']} · {entry['mtime']}"
    return (
        '<button class="action preview-trigger" type="button" data-preview-trigger="true" '
        f'data-preview-kind="{entry["preview_kind"]}" '
        f'data-preview-url="{entry["raw"]}" '
        f'data-preview-name="{html.escape(entry["name"], quote=True)}" '
        f'data-preview-meta="{html.escape(meta, quote=True)}">预览</button>'
    )



def load_template() -> Template:
    return Template(TEMPLATE_PATH.read_text(encoding='utf-8'))



def render_listing_page(rel_display: str, count_dirs: int, count_files: int, content: str) -> bytes:
    template = load_template()
    body = template.substitute(
        title=TITLE,
        rel_display=html.escape(rel_display),
        count_dirs=str(count_dirs),
        count_files=str(count_files),
        content=content,
        root_path=html.escape(str(ROOT)),
        prefix=BASE_PREFIX,
    )
    return body.encode('utf-8')


class Handler(BaseHTTPRequestHandler):
    server_version = 'wanyue'

    def do_HEAD(self):
        path = self.path.split('?', 1)[0]
        normalized = normalize_request_path(self.path)
        if path.startswith(STATIC_PREFIX):
            self.serve_static(path[len(STATIC_PREFIX):], headers_only=True)
            return
        if normalized.startswith(STATIC_PREFIX):
            self.serve_static(normalized[len(STATIC_PREFIX):], headers_only=True)
            return

        if normalized.startswith('/download/'):
            self.serve_file(normalized[len('/download/'):], headers_only=True, as_attachment=True)
        elif normalized.startswith('/raw/'):
            self.serve_file(normalized[len('/raw/'):], headers_only=True, as_attachment=False)
        else:
            self.serve_listing(normalized, headers_only=True)

    def do_GET(self):
        path = self.path.split('?', 1)[0]
        normalized = normalize_request_path(self.path)
        if path.startswith(STATIC_PREFIX):
            self.serve_static(path[len(STATIC_PREFIX):])
            return
        if normalized.startswith(STATIC_PREFIX):
            self.serve_static(normalized[len(STATIC_PREFIX):])
            return

        if normalized.startswith('/download/'):
            self.serve_file(normalized[len('/download/'):], as_attachment=True)
        elif normalized.startswith('/raw/'):
            self.serve_file(normalized[len('/raw/'):], as_attachment=False)
        else:
            self.serve_listing(normalized)

    def serve_listing(self, url_path: str, headers_only: bool = False):
        try:
            target = safe_target(url_path)
        except PermissionError:
            self.send_error(403)
            return

        if target.is_file():
            self.serve_file(url_path.lstrip('/'), as_attachment=True)
            return
        if not target.exists() or not target.is_dir():
            self.send_error(404)
            return

        rel = target.relative_to(ROOT)
        rel_display = '/' + rel.as_posix() if str(rel) != '.' else '/'
        entries = []
        for child in sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
            st = child.stat()
            child_rel = child.relative_to(ROOT).as_posix()
            is_dir = child.is_dir()
            preview_kind = None if is_dir else get_preview_kind(child)
            entries.append({
                'name': child.name,
                'href': BASE_PREFIX + '/' + quote(child_rel) + ('/' if is_dir else ''),
                'download': BASE_PREFIX + '/download/' + quote(child_rel),
                'raw': BASE_PREFIX + '/raw/' + quote(child_rel),
                'type': '目录' if is_dir else '文件',
                'size': '—' if is_dir else fmt_size(st.st_size),
                'mtime': datetime.fromtimestamp(st.st_mtime).strftime('%Y-%m-%d %H:%M'),
                'sub': '打开' if is_dir else '下载',
                'preview_kind': preview_kind,
                'icon': get_icon(is_dir),
            })

        count_files = sum(1 for e in entries if e['type'] == '文件')
        count_dirs = sum(1 for e in entries if e['type'] == '目录')
        up_link = None
        if str(rel) != '.':
            parent_rel = rel.parent.as_posix()
            up_link = BASE_PREFIX + '/' + (quote(parent_rel) + '/' if parent_rel != '.' else '')

        rows = []
        if up_link:
            rows.append(
                '<tr>'
                f'<td>{render_name_cell("..", "返回上一级", up_link, get_icon(False, is_parent=True))}</td>'
                '<td class="type">目录</td><td class="muted">—</td><td class="muted">—</td>'
                '<td><div class="actions"><a class="action" href="{0}">打开</a></div></td>'
                '</tr>'.format(up_link)
            )
        for entry in entries:
            actions = [render_preview_button(entry)] if entry['preview_kind'] else []
            is_file = entry['type'] == '文件'
            preview_kind = entry.get('preview_kind')

            # Build name cell: for files, sub = "预览" if previewable else ""
            # For files, href = "#" and add data-preview-* attrs if previewable
            if is_file:
                sub_text = '预览' if preview_kind else '文件'
                href = '#'
                extra = ''
                if preview_kind:
                    meta = f"{entry['type']} · {entry['size']} · {entry['mtime']}"
                    extra = (
                        f' data-preview-trigger="true"'
                        f' data-preview-kind="{preview_kind}"'
                        f' data-preview-url="{entry["raw"]}"'
                        f' data-preview-name="{html.escape(entry["name"], quote=True)}"'
                        f' data-preview-meta="{html.escape(meta, quote=True)}"'
                    )
                name_cell = render_name_cell(entry['name'], sub_text, href, entry['icon'], extra)
            else:
                name_cell = render_name_cell(entry['name'], entry['sub'], entry['href'], entry['icon'])

            if is_file:
                actions.append(f'<a class="download" href="{entry["download"]}" data-download-trigger="true">下载</a>')
            else:
                actions.append(f'<a class="action" href="{entry["href"]}">打开</a>')
            rows.append(
                '<tr>'
                f'<td>{name_cell}</td>'
                f'<td class="type">{entry["type"]}</td>'
                f'<td class="muted">{entry["size"]}</td>'
                f'<td class="muted">{entry["mtime"]}</td>'
                f'<td><div class="actions">{"".join(actions)}</div></td>'
                '</tr>'
            )

        content = ''.join(rows) if rows else '<div class="empty">当前目录为空</div>'
        if rows:
            content = (
                '<table class="table"><thead><tr><th>名称</th><th>类型</th><th>大小</th>'
                '<th>更新时间</th><th>操作</th></tr></thead><tbody>' + content + '</tbody></table>'
            )

        data = render_listing_page(rel_display, count_dirs, count_files, content)
        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.send_header('Content-Length', str(len(data)))
        self.end_headers()
        if not headers_only:
            self.wfile.write(data)

    def serve_static(self, rel_path: str, headers_only: bool = False):
        rel = posixpath.normpath('/' + unquote(rel_path)).lstrip('/')
        target = (STATIC_DIR / rel).resolve()
        if target == STATIC_DIR or STATIC_DIR not in target.parents or not target.exists() or not target.is_file():
            self.send_error(404)
            return

        ctype = mimetypes.guess_type(str(target))[0] or 'application/octet-stream'
        fs = target.stat()
        self.send_response(200)
        self.send_header('Content-Type', ctype)
        self.send_header('Content-Length', str(fs.st_size))
        self.send_header('Cache-Control', 'no-cache')
        self.end_headers()
        if headers_only:
            return
        with target.open('rb') as handle:
            self.wfile.write(handle.read())

    def serve_file(self, rel_path: str, headers_only: bool = False, as_attachment: bool = True):
        try:
            target = safe_target(rel_path)
        except PermissionError:
            self.send_error(403)
            return
        if not target.exists() or not target.is_file():
            self.send_error(404)
            return

        preview_kind = get_preview_kind(target)
        ctype = mimetypes.guess_type(str(target))[0] or 'application/octet-stream'
        if not as_attachment and preview_kind in {'code', 'plain'}:
            ctype = 'text/plain; charset=utf-8'

        # For markdown preview, render to HTML server-side
        payload = None
        if not as_attachment and preview_kind == 'markdown':
            md_text = target.read_text(encoding='utf-8')
            html_body = markdown.markdown(md_text, extensions=['extra', 'codehilite', 'toc'])
            payload = html_body.encode('utf-8')
            ctype = 'text/html; charset=utf-8'

        fs = target.stat()
        self.send_response(200)
        self.send_header('Content-Type', ctype)
        self.send_header('Content-Length', str(len(payload)) if payload else str(fs.st_size))
        self.send_header('X-Content-Type-Options', 'nosniff')
        disposition = 'attachment' if as_attachment else 'inline'
        ascii_name = ''.join(
            ch if 32 <= ord(ch) < 127 and ch not in {'\\', '"'} else '_'
            for ch in target.name
        ).strip(' .')
        if not ascii_name:
            ascii_name = 'download'
        encoded_name = quote(target.name)
        self.send_header(
            'Content-Disposition',
            f'{disposition}; filename="{ascii_name}"; filename*=UTF-8\'\'{encoded_name}'
        )
        self.end_headers()
        if headers_only:
            return

        if payload:
            self.wfile.write(payload)
            return

        with target.open('rb') as handle:
            while True:
                chunk = handle.read(1024 * 1024)
                if not chunk:
                    break
                self.wfile.write(chunk)

    def log_message(self, format, *args):
        return


if __name__ == '__main__':
    ThreadingHTTPServer(('127.0.0.1', 8091), Handler).serve_forever()
