from __future__ import annotations

import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any


VALID_DOMAINS = {"backend", "frontend", "ux", "n8n", "product", "workflow"}
VALID_BUCKETS = {"patterns", "risques"}
INDEX_VERSION = 1
INDEX_FILENAME = ".leadtech_mcp_index.json"


@dataclass(frozen=True)
class LeadtechPaths:
    root: Path
    knowledge: Path
    capitalisation: Path
    projects_conf: Path


@dataclass(frozen=True)
class KnowledgeDocument:
    path: Path
    metadata: dict[str, Any]
    body: str


def get_paths() -> LeadtechPaths:
    configured_root = os.getenv("LEADTECH_ROOT")
    if configured_root:
        root = Path(configured_root).resolve()
    else:
        default_root = Path("/srv/helpers/_Assistant_Lead_Tech").resolve()
        root = default_root if default_root.exists() else Path(__file__).resolve().parents[4]
    return LeadtechPaths(
        root=root,
        knowledge=root / "knowledge",
        capitalisation=root / "95_a_capitaliser.md",
        projects_conf=root / "_projects.conf",
    )


def _safe_path(base: Path, *parts: str) -> Path:
    path = (base / Path(*parts)).resolve()
    if not str(path).startswith(str(base.resolve())):
        raise ValueError("Chemin hors base autorisee")
    return path


def list_domain_files(domain: str, bucket: str) -> list[Path]:
    if domain not in VALID_DOMAINS:
        raise ValueError(f"Domaine invalide: {domain}")
    if bucket not in VALID_BUCKETS:
        raise ValueError(f"Type invalide: {bucket}")
    base = _safe_path(get_paths().knowledge, domain, bucket)
    if not base.exists():
        return []
    return sorted(p for p in base.glob("*.md") if p.is_file())


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


EXCERPT_LENGTH = 400


def _coerce_metadata_scalar(value: str) -> Any:
    raw = value.strip()
    if not raw:
        return ""

    low = raw.lower()
    if low in {"true", "false"}:
        return low == "true"

    if raw.startswith("[") and raw.endswith("]"):
        inner = raw[1:-1].strip()
        if not inner:
            return []
        return [item.strip().strip("'\"") for item in inner.split(",") if item.strip()]

    return raw.strip("'\"")


def parse_front_matter(content: str) -> tuple[dict[str, Any], str]:
    if not content.startswith("---\n"):
        return {}, content

    lines = content.splitlines()
    end_idx = None
    for idx in range(1, len(lines)):
        if lines[idx].strip() == "---":
            end_idx = idx
            break

    if end_idx is None:
        return {}, content

    metadata: dict[str, Any] = {}
    for line in lines[1:end_idx]:
        stripped = line.strip()
        if not stripped or stripped.startswith("#") or ":" not in stripped:
            continue
        key, value = stripped.split(":", 1)
        metadata[key.strip()] = _coerce_metadata_scalar(value)

    marker = "\n---\n"
    _, _, tail = content.partition(marker)
    body = tail.lstrip("\n")
    return metadata, body


def read_knowledge_document(path: Path) -> KnowledgeDocument:
    raw = read_text(path)
    metadata, body = parse_front_matter(raw)
    return KnowledgeDocument(path=path, metadata=metadata, body=body)


def get_index_path(paths: LeadtechPaths | None = None) -> Path:
    resolved_paths = paths or get_paths()
    return resolved_paths.root / INDEX_FILENAME


def _extract_excerpt(content: str, tokens: list[str]) -> str:
    """Retourne un extrait centré sur la première occurrence d'un token, ou le début du fichier."""
    low = content.lower()
    best_pos = len(content)
    for tok in tokens:
        pos = low.find(tok)
        if 0 <= pos < best_pos:
            best_pos = pos
    if best_pos == len(content):
        best_pos = 0
    start = max(0, best_pos - 80)
    excerpt = content[start : start + EXCERPT_LENGTH].strip()
    if start > 0:
        excerpt = "…" + excerpt
    if start + EXCERPT_LENGTH < len(content):
        excerpt = excerpt + "…"
    return excerpt


def _metadata_search_text(metadata: dict[str, Any]) -> str:
    parts: list[str] = []
    for value in metadata.values():
        if isinstance(value, list):
            parts.extend(str(item) for item in value)
        else:
            parts.append(str(value))
    return " ".join(parts).lower()


def _score_text(content: str, tokens: list[str]) -> int:
    low = content.lower()
    return sum(low.count(tok) for tok in tokens)


def _build_knowledge_entry(paths: LeadtechPaths, file_path: Path, bucket: str, domain: str) -> dict[str, Any]:
    doc = read_knowledge_document(file_path)
    return {
        "kind": "knowledge",
        "domain": domain,
        "bucket": bucket,
        "relative_path": str(file_path.relative_to(paths.root)),
        "title": str(doc.metadata.get("title", file_path.stem)),
        "metadata": doc.metadata,
        "body": doc.body,
    }


def _build_global_entry(paths: LeadtechPaths, filename: str, label: str) -> dict[str, Any] | None:
    file_path = paths.root / filename
    if not file_path.exists():
        return None
    return {
        "kind": "global",
        "label": label,
        "filename": filename,
        "relative_path": str(file_path.relative_to(paths.root)),
        "content": read_text(file_path),
    }


def build_search_index(paths: LeadtechPaths | None = None) -> dict[str, Any]:
    resolved_paths = paths or get_paths()
    entries: list[dict[str, Any]] = []

    for domain in sorted(VALID_DOMAINS):
        for bucket in sorted(VALID_BUCKETS):
            for file_path in list_domain_files(domain, bucket):
                entries.append(_build_knowledge_entry(resolved_paths, file_path, bucket, domain))

    for filename, label in _GLOBAL_DOCS:
        entry = _build_global_entry(resolved_paths, filename, label)
        if entry is not None:
            entries.append(entry)

    return {
        "version": INDEX_VERSION,
        "root": str(resolved_paths.root),
        "entries": entries,
    }


def write_search_index(paths: LeadtechPaths | None = None, output_path: Path | None = None) -> Path:
    resolved_paths = paths or get_paths()
    target = output_path or get_index_path(resolved_paths)
    payload = build_search_index(resolved_paths)
    target.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return target


def load_search_index(paths: LeadtechPaths | None = None) -> dict[str, Any] | None:
    resolved_paths = paths or get_paths()
    index_path = get_index_path(resolved_paths)
    if not index_path.exists():
        return None

    try:
        payload = json.loads(index_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return None

    if payload.get("version") != INDEX_VERSION:
        return None

    entries = payload.get("entries")
    if not isinstance(entries, list):
        return None

    return payload


def _search_knowledge_from_entries(root: Path, entries: list[dict[str, Any]], domain: str, buckets: list[str], tokens: list[str], max_items: int) -> list[dict[str, str]]:
    out: list[dict[str, str]] = []

    for entry in entries:
        if entry.get("kind") != "knowledge":
            continue
        if entry.get("domain") != domain:
            continue
        if entry.get("bucket") not in buckets:
            continue

        metadata = entry.get("metadata", {})
        body = str(entry.get("body", ""))
        score = _score_text(body, tokens)
        score += _score_text(_metadata_search_text(metadata), tokens) * 3
        if score <= 0:
            continue

        relative_path = Path(str(entry["relative_path"]))
        out.append(
            {
                "path": str((root / relative_path).resolve()),
                "bucket": str(entry["bucket"]),
                "title": str(entry.get("title", relative_path.stem)),
                "score": str(score),
                "excerpt": _extract_excerpt(body, tokens),
                "tags": ", ".join(metadata.get("tags", [])),
                "severity": str(metadata.get("severity", "")),
                "applies_to": ", ".join(metadata.get("applies_to", [])),
            }
        )

    out.sort(key=lambda x: int(x["score"]), reverse=True)
    return out[:max_items]


def _search_global_from_entries(root: Path, entries: list[dict[str, Any]], tokens: list[str], max_items: int) -> list[dict[str, str]]:
    out: list[dict[str, str]] = []

    for entry in entries:
        if entry.get("kind") != "global":
            continue

        content = str(entry.get("content", ""))
        score = _score_text(content, tokens)
        if score <= 0:
            continue

        relative_path = Path(str(entry["relative_path"]))
        out.append(
            {
                "path": str((root / relative_path).resolve()),
                "bucket": "global",
                "title": str(entry["label"]),
                "filename": str(entry["filename"]),
                "score": str(score),
                "excerpt": _extract_excerpt(content, tokens),
            }
        )

    out.sort(key=lambda x: int(x["score"]), reverse=True)
    return out[:max_items]


def _validate_slug(slug: str) -> str:
    path = Path(slug)
    if path.name != slug or any(part in {"..", "."} for part in path.parts):
        raise ValueError("Chemin hors base autorisee: slug invalide")
    return slug


def search_knowledge(domain: str, query: str, bucket: str | None = None, max_items: int = 6) -> list[dict[str, str]]:
    paths = get_paths()
    buckets = [bucket] if bucket else ["patterns", "risques"]
    tokens = [t.strip().lower() for t in query.split() if t.strip()]
    index = load_search_index(paths)
    if index is not None:
        return _search_knowledge_from_entries(paths.root, index["entries"], domain, buckets, tokens, max_items)

    out: list[dict[str, str]] = []
    for b in buckets:
        for file_path in list_domain_files(domain, b):
            doc = read_knowledge_document(file_path)
            score = _score_text(doc.body, tokens)
            score += _score_text(_metadata_search_text(doc.metadata), tokens) * 3
            if score <= 0:
                continue
            out.append(
                {
                    "path": str(file_path),
                    "bucket": b,
                    "title": str(doc.metadata.get("title", file_path.stem)),
                    "score": str(score),
                    "excerpt": _extract_excerpt(doc.body, tokens),
                    "tags": ", ".join(doc.metadata.get("tags", [])),
                    "severity": str(doc.metadata.get("severity", "")),
                    "applies_to": ", ".join(doc.metadata.get("applies_to", [])),
                }
            )

    out.sort(key=lambda x: int(x["score"]), reverse=True)
    return out[:max_items]


def read_knowledge_doc(domain: str, bucket: str, slug: str) -> str:
    safe_slug = _validate_slug(slug)
    file_path = _safe_path(get_paths().knowledge, domain, bucket, f"{safe_slug}.md")
    if not file_path.exists():
        raise FileNotFoundError(f"Fichier introuvable: {file_path}")
    return read_text(file_path)


# Fichiers globaux indexés (hors knowledge/) — ordre de priorité
_GLOBAL_DOCS: list[tuple[str, str]] = [
    ("40_decisions_et_archi.md", "architecture"),
    ("90_debug_et_postmortem.md", "debug"),
    ("10_conventions_redaction.md", "conventions"),
]


def search_global_docs(query: str, max_items: int = 4) -> list[dict[str, str]]:
    """Cherche dans les fichiers globaux Lead_tech (decisions, postmortems, conventions)."""
    tokens = [t.strip().lower() for t in query.split() if t.strip()]
    paths = get_paths()
    index = load_search_index(paths)
    if index is not None:
        return _search_global_from_entries(paths.root, index["entries"], tokens, max_items)

    root = paths.root
    out: list[dict[str, str]] = []

    for filename, label in _GLOBAL_DOCS:
        file_path = root / filename
        if not file_path.exists():
            continue
        content = read_text(file_path)
        score = _score_text(content, tokens)
        if score <= 0:
            continue
        out.append(
            {
                "path": str(file_path),
                "bucket": "global",
                "title": label,
                "filename": filename,
                "score": str(score),
                "excerpt": _extract_excerpt(content, tokens),
            }
        )

    out.sort(key=lambda x: int(x["score"]), reverse=True)
    return out[:max_items]
