#!/usr/bin/env python3
"""job2026.apps.udalovs.ru — минимальный сайт-браузер структурированных материалов.

Читает markdown прямо из репозитория (без сборки): папки — как списки-карточки,
.md файлы — как свёрстанные страницы. Живёт на systemd (job2026-site.service).
"""
import html
import json
import os
import re
import time
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

import markdown
from pygments.formatters import HtmlFormatter

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_DIR = os.path.join(BASE_DIR, "static")
FONTS_DIR = "/mnt/storage/projects/new-job-2026/profile/assets/fonts"
PORT = 8090

SECTIONS = [
    {
        "slug": "system-design",
        "title": "System Design",
        "subtitle": "Подготовка к архитектурной секции · Yandex Codenv, этап 2",
        "root": "/mnt/storage/projects/new-job-2026/applications/yandex-codenv/system-design",
    },
]

MD_EXTS = ["tables", "fenced_code", "codehilite", "sane_lists", "attr_list"]
MD_CFG = {"codehilite": {"guess_lang": False}}

# ---------------------------------------------------------------- helpers


def ensure_pygments_css():
    css_path = os.path.join(STATIC_DIR, "pygments.css")
    if not os.path.exists(css_path):
        with open(css_path, "w") as f:
            f.write(HtmlFormatter(style="default").get_style_defs(".codehilite"))


def read_md(path):
    with open(path, encoding="utf-8", errors="replace") as f:
        return f.read()


def md_meta(text):
    """(title, description) из markdown: первый H1 и первый абзац."""
    title, desc = None, None
    for line in text.splitlines():
        s = line.strip()
        if not s:
            continue
        if title is None and (s.startswith("# ") or s.startswith("## ")):
            title = re.sub(r"^#+\s*", "", s).strip()
            continue
        if title is not None and not s.startswith(("#", "|", "```", "-", "*", ">")):
            desc = re.sub(r"[*_`#\[\]()!]", "", s)[:160]
            break
    return title or "(без заголовка)", desc or ""


def human_size(n):
    for unit in ("Б", "КБ", "МБ"):
        if n < 1024 or unit == "МБ":
            return f"{n:.0f} {unit}" if unit == "Б" else f"{n / 1024:.0f} {unit}"
        n /= 1024
    return f"{n:.0f}"


def page(title, body, crumb="", sidebar="", raw_url=None):
    raw = f'<a class="raw-link" href="{html.escape(raw_url)}" title="Открыть исходный markdown">md</a>' if raw_url else ""
    return f"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)} · Job 2026</title>
<link rel="stylesheet" href="/static/site.css">
<link rel="stylesheet" href="/static/pygments.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🧭</text></svg>">
</head>
<body>
<header class="topbar">
  <a class="brand" href="/">Job <span>2026</span></a>
  <nav class="crumbs">{crumb}</nav>
  {raw}
</header>
<div class="layout">
  <aside class="sidebar">{sidebar}</aside>
  <main class="content">{body}</main>
</div>
<footer>контент читается напрямую из репозитория · обновляется в реальном времени · {datetime.now(timezone.utc).strftime('%d.%m.%Y %H:%M')} UTC</footer>
</body>
</html>"""


def build_sidebar(section, current_rel):
    """Дерево раздела: корневые .md + подпапки (список их .md)."""
    root = section["root"]

    def rel_link(rel, name, is_dir=False):
        href = f"/{section['slug']}/{rel}" if rel else f"/{section['slug']}/"
        cls = "active" if rel == current_rel else ""
        icon = "📁" if is_dir else "📄"
        return f'<a class="{cls}" href="{html.escape(href)}">{icon} {html.escape(name)}</a>'

    items = []
    for name in sorted(os.listdir(root)):
        p = os.path.join(root, name)
        if os.path.isdir(p) and not name.startswith("."):
            mds = sorted(f for f in os.listdir(p) if f.endswith(".md") and not f.startswith("."))
            inner = "".join(
                f'<li>{rel_link(f"{name}/{f}", f[:-3] if f != "README.md" else f)}</li>' for f in mds
            )
            open_attr = " open" if current_rel.startswith(name + "/") else ""
            items.append(
                f'<details{open_attr}><summary>📁 {html.escape(name)} <span class="count">{len(mds)}</span></summary>'
                f'<ul>{inner}</ul></details>'
            )
        elif name.endswith(".md") and not name.startswith("."):
            items.append(f'<li>{rel_link(name, name[:-3])}</li>')
    return f'<nav class="tree"><div class="tree-title">{html.escape(section["title"])}</div><ul>{"".join(items)}</ul></nav>'


def render_listing(section, rel, fs_path):
    """Страница-список для папки: карточки файлов и подпапок."""
    entries = []
    for name in sorted(os.listdir(fs_path)):
        if name.startswith("."):
            continue
        p = os.path.join(fs_path, name)
        st = os.stat(p)
        rel_child = f"{rel}/{name}" if rel else name
        if os.path.isdir(p):
            n = sum(1 for f in os.listdir(p) if f.endswith(".md") and not f.startswith("."))
            entries.append({"dir": True, "name": name, "href": f"/{section['slug']}/{rel_child}/",
                            "title": name, "desc": f"{n} markdown-файлов" if n else "папка",
                            "size": "", "mtime": st.st_mtime})
        elif name.endswith(".md"):
            title, desc = md_meta(read_md(p))
            pin = name == "README.md"
            entries.append({"dir": False, "name": name, "href": f"/{section['slug']}/{rel_child}",
                            "title": ("README · " if pin else "") + title, "desc": desc,
                            "size": human_size(st.st_size),
                            "mtime": st.st_mtime, "pin": pin})
    entries.sort(key=lambda e: (not e.get("pin", False), not e["dir"], e["name"]))

    cards = []
    for e in entries:
        icon = "📁" if e["dir"] else "📄"
        cards.append(
            f'<a class="card" href="{html.escape(e["href"])}">'
            f'<div class="card-head">{icon} <span class="card-title">{html.escape(e["title"])}</span></div>'
            f'<div class="card-desc">{html.escape(e["desc"])}</div>'
            f'<div class="card-meta">{html.escape(e["size"])}{" · " if e["size"] else ""}'
            f'{datetime.fromtimestamp(e["mtime"], tz=timezone.utc).strftime("%d.%m.%Y")}</div></a>'
        )
    heading = "Материалы" if rel == "" else html.escape(rel)
    body = f'<h1 class="page-title">{heading}</h1><div class="cards">{"".join(cards)}</div>'
    return body


def render_md(section, rel, fs_path):
    text = read_md(fs_path)
    body_md = markdown.markdown(text, extensions=MD_EXTS, extension_configs=MD_CFG)
    return f'<article class="md">{body_md}</article>'


# ---------------------------------------------------------------- handler


class Handler(BaseHTTPRequestHandler):
    server_version = "job2026-site/1.0"

    def log_message(self, fmt, *args):
        pass  # тишина в journald

    def _send(self, code, content, ctype="text/html; charset=utf-8", cache="no-store"):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(content)))
        self.send_header("Cache-Control", cache)
        self.end_headers()
        self.wfile.write(content)

    def _html(self, code, s):
        self._send(code, s.encode("utf-8"))

    def do_GET(self):
        path, _, query = self.path.partition("?")
        path = path.split("#")[0].rstrip("/") or "/"
        raw = "raw=1" in query

        if path == "/":
            return self.home()
        if path.startswith("/static/"):
            return self.static(path[len("/static/"):], STATIC_DIR, 300)
        if path.startswith("/fonts/"):
            return self.static(path[len("/fonts/"):], FONTS_DIR, 604800)

        for sec in SECTIONS:
            if path == f"/{sec['slug']}" or path.startswith(f"/{sec['slug']}/"):
                return self.section(sec, path[len(sec["slug"]) + 2:], raw)
        return self._html(404, page("404", "<h1>404</h1><p>Нет такой страницы.</p>"))

    def home(self):
        cards = []
        for sec in SECTIONS:
            n = sum(len(files) for _, _, files in os.walk(sec["root"]) if files)
            cards.append(
                f'<a class="card section-card" href="/{sec["slug"]}/">'
                f'<div class="card-head">🗂 <span class="card-title">{html.escape(sec["title"])}</span></div>'
                f'<div class="card-desc">{html.escape(sec["subtitle"])}</div>'
                f'<div class="card-meta">{n} markdown-файлов</div></a>'
            )
        body = ('<h1 class="page-title">База знаний · поиск работы 2026</h1>'
                '<p class="lede">Структурированные материалы: md — как страницы, папки — как списки.</p>'
                f'<div class="cards">{"".join(cards)}</div>')
        self._html(200, page("База знаний", body))

    def static(self, name, base, cache_s):
        p = os.path.realpath(os.path.join(base, name.lstrip("/")))
        if not p.startswith(os.path.realpath(base)) or not os.path.isfile(p):
            return self._send(404, b"not found", "text/plain")
        with open(p, "rb") as f:
            data = f.read()
        ctype = ("text/css; charset=utf-8" if p.endswith(".css")
                 else "font/ttf" if p.endswith(".ttf") else "application/octet-stream")
        self._send(200, data, ctype, f"public, max-age={cache_s}")

    def section(self, sec, rel, raw):
        rel = rel.strip("/")
        root = os.path.realpath(sec["root"])
        fs = os.path.realpath(os.path.join(root, rel))
        if not fs.startswith(root):
            return self._html(404, page("404", "<h1>404</h1>"))

        if os.path.isdir(fs):
            if raw:
                return self.listing_raw(fs)
            body = render_listing(sec, rel, fs)
            side = build_sidebar(sec, rel)
            crumb = self.crumbs(sec, rel)
            return self._html(200, page(f"{sec['title']} · {rel or 'обзор'}", body, crumb, side))

        if os.path.isfile(fs):
            if fs.endswith(".pdf"):
                # PDF (книги и пр.) публично не отдаём — только в репо/инбоксе
                return self._html(404, page("404", "<h1>404</h1><p>Нет такой страницы.</p>"))
            if fs.endswith(".md") and not raw:
                body = render_md(sec, rel, fs)
                side = build_sidebar(sec, os.path.dirname(rel))
                crumb = self.crumbs(sec, rel)
                return self._html(200, page(f"{os.path.basename(rel)[:-3]} · {sec['title']}", body, crumb, side,
                                            raw_url=f"/{sec['slug']}/{rel}?raw=1"))
            ctype = "text/markdown; charset=utf-8" if fs.endswith(".md") else "application/octet-stream"
            with open(fs, "rb") as f:
                return self._send(200, f.read(), ctype)
        return self._html(404, page("404", "<h1>404</h1><p>Нет такой страницы.</p>"))

    def listing_raw(self, fs):
        items = [n for n in sorted(os.listdir(fs)) if not n.startswith(".")]
        self._send(200, json.dumps(items, ensure_ascii=False, indent=2).encode(), "application/json")

    @staticmethod
    def crumbs(sec, rel):
        parts = [f'<a href="/">job2026</a>', f'<a href="/{sec["slug"]}/">{html.escape(sec["title"])}</a>']
        acc = ""
        for chunk in filter(None, rel.split("/")):
            acc += "/" + chunk
            parts.append(f'<a href="/{sec["slug"]}{acc}">{html.escape(chunk)}</a>')
        return ' <span class="sep">/</span> '.join(parts)


if __name__ == "__main__":
    ensure_pygments_css()
    os.makedirs(STATIC_DIR, exist_ok=True)
    print(f"job2026-site on :{PORT}")
    ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
