from __future__ import annotations

import json
import hashlib
from dataclasses import dataclass
from pathlib import Path
from typing import List, Dict, Any, Optional
import zipfile
import re
from importlib import metadata as importlib_metadata
from markdown_it import MarkdownIt


@dataclass
class DocFile:
    relpath: str
    abspath: Path
    text: str
    links: List[str]


def _read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8", errors="ignore")


def _sha256_text(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8", errors="ignore")).hexdigest()


def _get_attr(token, key: str) -> Optional[str]:
    """兼容 markdown-it-py 的 attrs 结构：可能是 dict，也可能是 [(k,v), ...]"""
    attrs = getattr(token, "attrs", None)
    if not attrs:
        return None

    if isinstance(attrs, dict):
        v = attrs.get(key)
        return v if isinstance(v, str) and v else None

    # 兼容 list/tuple of pairs
    try:
        for item in attrs:
            if isinstance(item, (list, tuple)) and len(item) >= 2:
                k, v = item[0], item[1]
                if k == key and isinstance(v, str) and v:
                    return v
    except TypeError:
        return None

    return None


def _extract_links_md(text: str) -> List[str]:
    """从 Markdown 里抽取链接（MVP：抽出 href，不做判断）"""
    md = MarkdownIt()
    tokens = md.parse(text)
    links: List[str] = []

    def walk(ts):
        for t in ts:
            if t.type == "link_open":
                href = _get_attr(t, "href")
                if href:
                    links.append(href)

            # 关键：Markdown 链接常在 inline token 的 children 里
            children = getattr(t, "children", None)
            if children:
                walk(children)

    walk(tokens)
    return links


def _collect_doc_files(repo: Path) -> List[Path]:
    """MVP：只收 README*.md + docs/**/*.md"""
    files: List[Path] = []

    for name in ["README.md", "README.MD", "readme.md", "Readme.md"]:
        p = repo / name
        if p.exists() and p.is_file():
            files.append(p)

    docs_dir = repo / "docs"
    if docs_dir.exists() and docs_dir.is_dir():
        files.extend(sorted(docs_dir.rglob("*.md")))

    uniq: List[Path] = []
    seen = set()
    for f in files:
        rp = str(f.resolve())
        if rp not in seen:
            uniq.append(f)
            seen.add(rp)
    return uniq


def _make_tree_text(relpaths: List[str]) -> str:
    tree: Dict[str, Any] = {}

    def add_path(path: str):
        parts = path.split("/")
        node = tree
        for part in parts[:-1]:
            node = node.setdefault(part, {})
        node.setdefault("__files__", []).append(parts[-1])

    def render(node: Dict[str, Any], indent: int = 0) -> List[str]:
        lines: List[str] = []
        dirs = sorted([k for k in node.keys() if k != "__files__"])
        files = sorted(node.get("__files__", []))
        for d in dirs:
            lines.append("  " * indent + f"- {d}/")
            lines.extend(render(node[d], indent + 1))
        for f in files:
            lines.append("  " * indent + f"- {f}")
        return lines

    for rp in relpaths:
        add_path(rp)

    lines = render(tree, 0)
    return ("\n".join(lines) + "\n") if lines else ""

def _anchors_in_markdown(text: str) -> set[str]:
    """非常粗糙的锚点提取：把 '# Title' 变成 'title' 这种 slug（够 MVP 用）"""
    anchors: set[str] = set()
    for line in text.splitlines():
        s = line.strip()
        if s.startswith("#"):
            # 去掉前面的 ### 和空格
            title = s.lstrip("#").strip()
            if not title:
                continue
            slug = (
                title.lower()
                .replace(" ", "-")
                .replace("/", "-")
            )
            anchors.add(slug)
    return anchors

def _resolve_relative_link(doc_rel: str, href: str) -> str:
    """
    把 './x.md' 这种相对链接解析成 repo 内的相对路径（POSIX 风格）
    doc_rel: 当前文档相对路径，如 'docs/setup.md'
    """
    base_dir = str(Path(doc_rel).parent).replace("\\", "/")
    # base_dir 可能是 '.'，处理成空
    if base_dir == ".":
        base_dir = ""
    joined = (Path(base_dir) / href).as_posix()
    # 规范化 ./ ../
    norm = Path(joined).as_posix()
    return norm

def _health_check(repo: Path, docfiles: List[DocFile], links_map: Dict[str, List[str]]) -> Dict[str, Any]:
    """MVP：只检查两类：缺文件、缺锚点（只处理 md 内部链接）"""
    # 方便查文件是否存在
    existing_paths = {d.relpath for d in docfiles}
    # 方便查某个文档有哪些 anchors
    anchors_map = {d.relpath: _anchors_in_markdown(d.text) for d in docfiles}

    issues: List[Dict[str, Any]] = []

    for doc_rel, hrefs in links_map.items():
        for href in hrefs:
            if href.startswith("#"):
                # 锚点检查
                anchor = href[1:]
                if anchor and anchor not in anchors_map.get(doc_rel, set()):
                    issues.append(
                        {"type": "missing_anchor", "doc": doc_rel, "target": href}
                    )
                continue
            # 只处理 repo 内相对 md 链接（MVP：以 .md 结尾或以 ./ ../ 开头）
            if href.startswith("http://") or href.startswith("https://"):
                continue
            if not (href.endswith(".md") or href.startswith("./") or href.startswith("../")):
                continue

            resolved = _resolve_relative_link(doc_rel, href)
            # 统一成 posix
            resolved = resolved.replace("\\", "/")

            if resolved not in existing_paths:
                issues.append(
                    {
                        "type": "missing_file",
                        "doc": doc_rel,
                        "target": href,
                        "resolved_path": resolved,
                    }
                )

    return {"schema": "ai-dockpack/health@0.1", "issues": issues}                            

def _read_tool_version() -> str:
    """
    读取“工具自身”的版本（ai-dockpack 工具项目版本）
    优先：工具项目根目录的 pyproject.toml
    兜底：importlib.metadata 读取已安装包版本
    """
    tool_root = Path(__file__).resolve().parents[2]  # .../ai-dockpack/
    pyproject = tool_root / "pyproject.toml"
    if pyproject.exists():
        text = pyproject.read_text(encoding="utf-8", errors="ignore")
        m = re.search(r'^\s*version\s*=\s*"([^"]+)"\s*$', text, flags=re.M)
        if m:
            return m.group(1).strip()

    try:
        return importlib_metadata.version("ai-dockpack")
    except Exception:
        return ""

def _parse_semver(v: str):
    """把 '1.2.3' -> (1,2,3)，解析失败返回 None（MVP）"""
    m = re.match(r"^\s*(\d+)\.(\d+)\.(\d+)\s*$", v)
    if not m:
        return None
    return tuple(int(x) for x in m.groups())

def _freshness_check(repo: Path, docfiles: List[DocFile]) -> Dict[str, Any]:
    """
    MVP：只做一种规则
    - 在文档中找 `ai-dockpack==X.Y.Z`
    - 若 X.Y.Z < 当前项目版本（repo/pyproject.toml 的 version），报告 outdated_pinned_version
    """
    current = _read_tool_version()
    current_sem = _parse_semver(current) if current else None

    issues: List[Dict[str, Any]] = []
    if not current_sem:
        return {"schema": "ai-dockpack/freshness@0.1", "current_version": current, "issues": issues}

    pattern = re.compile(r"ai-dockpack==(\d+\.\d+\.\d+)")

    for d in docfiles:
        for m in pattern.finditer(d.text):
            pinned = m.group(1)
            pinned_sem = _parse_semver(pinned)
            if pinned_sem and pinned_sem < current_sem:
                issues.append(
                    {
                        "type": "outdated_pinned_version",
                        "doc": d.relpath,
                        "found": pinned,
                        "current": current,
                    }
                )

    return {"schema": "ai-dockpack/freshness@0.1", "current_version": current, "issues": issues}        

def _slugify(title: str) -> str:
    s = title.strip().lower()
    s = s.replace("/", "-")
    s = "-".join(s.split())
    return s

def _split_markdown_by_headings(text: str) -> List[Dict[str, str]]:
    """
    极简切段：
    - 以 '#', '##', '###' 开头的行作为新段落起点
    - 每段返回：section(title)、anchor(slug)、content
    """
    lines = text.splitlines()
    chunks: List[Dict[str, str]] = []

    current_title = "Document"
    current_anchor = "document"
    buf: List[str] = []

    def flush():
        nonlocal buf
        content = "\n".join(buf).strip()
        if content:
            chunks.append(
                {"section": current_title, "anchor": current_anchor, "content": content}
            )
        buf = []

    for line in lines:
        s = line.strip()
        if s.startswith("#"):
            # 标题行：先把上一段 flush
            flush()
            title = s.lstrip("#").strip()
            current_title = title if title else "Untitled"
            current_anchor = _slugify(current_title)
            # 标题行本身也写进 chunk 内容（AI 读更自然）
            buf.append(line)
        else:
            buf.append(line)

    flush()
    return chunks    

def _render_report(manifest: Dict[str, Any], health: Dict[str, Any], freshness: Dict[str, Any]) -> str:
    doc_count = manifest.get("doc_count", 0)
    chunk_count = manifest.get("chunk_count", 0)

    health_issues = health.get("issues", []) or []
    fresh_issues = freshness.get("issues", []) or []

    lines: List[str] = []
    lines.append("# AI DocPack Report")
    lines.append("")
    lines.append("## Summary")
    lines.append(f"- docs: **{doc_count}**")
    lines.append(f"- chunks: **{chunk_count}**")
    lines.append(f"- health issues: **{len(health_issues)}**")
    lines.append(f"- freshness issues: **{len(fresh_issues)}**")
    lines.append("")

    lines.append("## Doc Health (broken / missing)")
    if not health_issues:
        lines.append("- ✅ no issues")
    else:
        for it in health_issues:
            t = it.get("type", "")
            doc = it.get("doc", "")
            target = it.get("target", "")
            if t == "missing_file":
                rp = it.get("resolved_path", "")
                lines.append(f"- ❌ **missing_file** in `{doc}` → `{target}` (resolved: `{rp}`)")
            elif t == "missing_anchor":
                lines.append(f"- ❌ **missing_anchor** in `{doc}` → `{target}`")
            else:
                lines.append(f"- ❌ **{t}** in `{doc}` → `{target}`")
    lines.append("")

    lines.append("## Freshness (possibly outdated)")
    if not fresh_issues:
        lines.append("- ✅ no issues")
    else:
        for it in fresh_issues:
            t = it.get("type", "")
            doc = it.get("doc", "")
            found = it.get("found", "")
            current = it.get("current", freshness.get("current_version", ""))
            if t == "outdated_pinned_version":
                lines.append(f"- ⚠️ **outdated_pinned_version** in `{doc}`: found `{found}`, current `{current}`")
            else:
                lines.append(f"- ⚠️ **{t}** in `{doc}`")
    lines.append("")
    return "\n".join(lines)

def build_docpack(repo_path: str, out_zip: str) -> Path:
    repo = Path(repo_path).resolve()
    if not repo.exists():
        raise FileNotFoundError(f"repo not found: {repo}")

    doc_paths = _collect_doc_files(repo)

    docfiles: List[DocFile] = []
    all_links: Dict[str, List[str]] = {}

    for p in doc_paths:
        rel = str(p.relative_to(repo)).replace("\\", "/")
        text = _read_text(p)
        links = _extract_links_md(text)

        docfiles.append(DocFile(relpath=rel, abspath=p, text=text, links=links))
        if links:
            all_links[rel] = links
    chunk_total = sum(len(_split_markdown_by_headings(d.text)) for d in docfiles)
    tree_txt = _make_tree_text([d.relpath for d in docfiles])

    manifest: Dict[str, Any] = {
        "schema": "ai-dockpack/manifest@0.1",
        "repo": str(repo),
        "doc_count": len(docfiles),
        "chunk_count": chunk_total,
        "docs": [
            {
                "path": d.relpath,
                "bytes": len(d.text.encode("utf-8", errors="ignore")),
                "sha256": _sha256_text(d.text),
                "mtime": int(d.abspath.stat().st_mtime),
                "links_count": len(d.links),
            }
            for d in docfiles
        ],
        "generated_at": __import__("datetime").datetime.utcnow().isoformat() + "Z",
    }

    out = Path(out_zip).resolve()

    with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as z:
        z.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
        z.writestr("tree.txt", tree_txt)
        z.writestr("links.json", json.dumps(all_links, ensure_ascii=False, indent=2))
        health = _health_check(repo, docfiles, all_links)
        z.writestr("health.json", json.dumps(health, ensure_ascii=False, indent=2))
        freshness = _freshness_check(repo, docfiles)
        z.writestr("freshness.json", json.dumps(freshness, ensure_ascii=False, indent=2))
        report_md = _render_report(manifest, health, freshness)
        z.writestr("report.md", report_md)

        chunk_total = 0
        for d in docfiles:
            parts = _split_markdown_by_headings(d.text)
            for idx, part in enumerate(parts, start=1):
                

                chunk_name = f"{d.relpath.replace('/', '__')}__{idx:03d}.txt"
                chunk_text = part["content"]
                chunk_sha = _sha256_text(chunk_text)

                header = (
                    "---\n"
                    f"source_path: {d.relpath}\n"
                    f"section: {part['section']}\n"
                    f"anchor: {part['anchor']}\n"
                    f"bytes: {len(chunk_text.encode('utf-8', errors='ignore'))}\n"
                    f"sha256: {chunk_sha}\n"
                    "---\n\n"
                )

                z.writestr(f"chunks/{chunk_name}", header + chunk_text + "\n")

    return out
