from __future__ import annotations

import hashlib
import re
import textwrap
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

_GEN_TIMESTAMP_RE = re.compile(
    r"<!-- generated by agentkit migrate -- ts:([0-9T:.+\-Z]+) hash:([a-f0-9]+) -->"
)

FORMAT_AGENTS_MD = "agents-md"
FORMAT_CLAUDE_MD = "claude-md"
FORMAT_AGENT_MD = "agent-md"
FORMAT_GEMINI_MD = "gemini-md"
FORMAT_COPILOT_MD = "copilot-md"
FORMAT_LLMSTXT = "llmstxt"
FORMAT_AGENTKIT_SOURCE = "agentkit-source"
FORMAT_ALL = "all"

DEDICATED_SOURCE_DIRNAME = ".agentkit"
DEDICATED_SOURCE_FILENAME = "source.md"

LEGACY_ALIASES = {
    "agents": FORMAT_AGENTS_MD,
    "agents-md": FORMAT_AGENTS_MD,
    "claude": FORMAT_CLAUDE_MD,
    "claude-md": FORMAT_CLAUDE_MD,
    "agent": FORMAT_AGENT_MD,
    "agent-md": FORMAT_AGENT_MD,
    "gemini": FORMAT_GEMINI_MD,
    "gemini-md": FORMAT_GEMINI_MD,
    "copilot": FORMAT_COPILOT_MD,
    "copilot-md": FORMAT_COPILOT_MD,
    "llmstxt": FORMAT_LLMSTXT,
    "llms": FORMAT_LLMSTXT,
    "llms.txt": FORMAT_LLMSTXT,
    "source": FORMAT_AGENTKIT_SOURCE,
    "agentkit-source": FORMAT_AGENTKIT_SOURCE,
    "canonical-source": FORMAT_AGENTKIT_SOURCE,
    "all": FORMAT_ALL,
    "auto": "auto",
}


@dataclass(frozen=True)
class ProjectionTarget:
    key: str
    filename: str
    label: str
    heading_style: str = "project"


TARGETS = {
    FORMAT_AGENTS_MD: ProjectionTarget(FORMAT_AGENTS_MD, "AGENTS.md", "AGENTS.md", "ops"),
    FORMAT_CLAUDE_MD: ProjectionTarget(FORMAT_CLAUDE_MD, "CLAUDE.md", "Claude", "project"),
    FORMAT_AGENT_MD: ProjectionTarget(FORMAT_AGENT_MD, "AGENT.md", "AGENT.md", "ops"),
    FORMAT_GEMINI_MD: ProjectionTarget(FORMAT_GEMINI_MD, "GEMINI.md", "Gemini", "project"),
    FORMAT_COPILOT_MD: ProjectionTarget(FORMAT_COPILOT_MD, "COPILOT.md", "Copilot", "project"),
    FORMAT_LLMSTXT: ProjectionTarget(FORMAT_LLMSTXT, "llms.txt", "llms.txt", "llms"),
}

KNOWN_FORMATS = tuple(TARGETS.keys())
FORMAT_FILENAMES = {key: target.filename for key, target in TARGETS.items()}
FILENAME_FORMATS = {target.filename: key for key, target in TARGETS.items()}
DETECTION_PRIORITY = (
    FORMAT_AGENTKIT_SOURCE,
    FORMAT_AGENTS_MD,
    FORMAT_CLAUDE_MD,
    FORMAT_AGENT_MD,
    FORMAT_GEMINI_MD,
    FORMAT_COPILOT_MD,
    FORMAT_LLMSTXT,
)


@dataclass
class ProjectionResult:
    source_format: str
    target_format: str
    source_path: str
    output_path: str
    content: str
    line_count: int = 0
    skipped: bool = False
    skip_reason: str = ""

    def __post_init__(self) -> None:
        if not self.line_count:
            self.line_count = len(self.content.splitlines())


@dataclass
class Section:
    level: int
    title: str
    body: str
    raw_heading: str


def normalize_format(value: str) -> str:
    key = value.lower().strip()
    if key not in LEGACY_ALIASES:
        raise ValueError(f"Unknown format: {value}")
    return LEGACY_ALIASES[key]


def _make_gen_stamp(source_content: str) -> str:
    ts = datetime.now(tz=timezone.utc).isoformat(timespec="seconds")
    h = hashlib.sha256(source_content.encode()).hexdigest()[:16]
    return f"<!-- generated by agentkit migrate -- ts:{ts} hash:{h} -->"


def _extract_gen_hash(content: str) -> Optional[str]:
    m = _GEN_TIMESTAMP_RE.search(content)
    return m.group(2) if m else None


def _source_hash(content: str) -> str:
    return hashlib.sha256(content.encode()).hexdigest()[:16]


def _parse_sections(text: str) -> list[Section]:
    sections: list[Section] = []
    lines = text.splitlines()
    intro_lines: list[str] = []
    current_heading: Optional[str] = None
    current_raw: Optional[str] = None
    current_level = 0
    body_lines: list[str] = []

    def flush() -> None:
        nonlocal intro_lines
        if current_heading is None:
            return
        sections.append(Section(current_level, current_heading, "\n".join(body_lines).strip(), current_raw or ""))

    for line in lines:
        m = re.match(r"^(#{1,6})\s+(.*)", line)
        if m:
            flush()
            current_level = len(m.group(1))
            current_heading = m.group(2).strip()
            current_raw = line
            body_lines = []
        elif current_heading is None:
            intro_lines.append(line)
        else:
            body_lines.append(line)
    flush()
    return sections


def _get_top_level_intro(text: str) -> str:
    lines = text.splitlines()
    intro: list[str] = []
    for line in lines:
        if re.match(r"^#{1,6}\s", line):
            break
        intro.append(line)
    return "\n".join(intro).strip()


def _detect_format_from_filename(filename: str) -> Optional[str]:
    return FILENAME_FORMATS.get(Path(filename).name)


def dedicated_source_path(directory: str | Path) -> Path:
    return Path(directory) / DEDICATED_SOURCE_DIRNAME / DEDICATED_SOURCE_FILENAME


def source_path_for_format(directory: str | Path, fmt: str) -> Path:
    if fmt == FORMAT_AGENTKIT_SOURCE:
        return dedicated_source_path(directory)
    return Path(directory) / FORMAT_FILENAMES[fmt]


def _detect_format_from_content(content: str) -> Optional[str]:
    if re.match(r"^#\s+\S", content) and re.search(r"(^-\s+https?://)|(Generated from:)", content, re.MULTILINE):
        return FORMAT_LLMSTXT
    lowered = content.lower()
    if "session startup" in lowered or "memory rules" in lowered:
        return FORMAT_AGENTS_MD
    if "copilot" in lowered and "instruction" in lowered:
        return FORMAT_COPILOT_MD
    if "gemini" in lowered and "workspace" in lowered:
        return FORMAT_GEMINI_MD
    if "overview" in lowered or "commands" in lowered or "patterns" in lowered:
        return FORMAT_CLAUDE_MD
    return None


class ContextProjectionEngine:
    def detect_format(self, path: str | Path) -> Optional[str]:
        p = Path(path)
        if p.name == DEDICATED_SOURCE_FILENAME and p.parent.name == DEDICATED_SOURCE_DIRNAME:
            return FORMAT_AGENTKIT_SOURCE
        fmt = _detect_format_from_filename(p.name)
        if fmt:
            return fmt
        try:
            return _detect_format_from_content(p.read_text(encoding="utf-8", errors="replace"))
        except OSError:
            return None

    def detect_source(self, directory: str | Path, override: Optional[str] = None) -> Optional[tuple[str, Path]]:
        d = Path(directory)
        if override:
            fmt = normalize_format(override)
            path = source_path_for_format(d, fmt)
            return (fmt, path) if path.exists() else None
        for fmt in DETECTION_PRIORITY:
            candidate = source_path_for_format(d, fmt)
            if candidate.exists():
                return fmt, candidate
        for candidate in sorted(d.iterdir() if d.exists() else []):
            if candidate.is_file():
                fmt = self.detect_format(candidate)
                if fmt:
                    return fmt, candidate
        return None

    def convert(self, source_content: str, source_format: str, target_format: str, source_path: str = "<source>", output_path: str = "<output>") -> ProjectionResult:
        if target_format not in TARGETS:
            raise ValueError(f"Unsupported conversion: {source_format} → {target_format}")
        content = self.project(source_content=source_content, source_format=source_format, target_format=target_format)
        return ProjectionResult(source_format, target_format, source_path, output_path, content)

    def convert_all(self, source_content: str, source_format: str, source_path: str = "<source>", directory: str | Path = ".") -> list[ProjectionResult]:
        root = Path(directory)
        return [
            self.convert(source_content, source_format, target, source_path, str(root / FORMAT_FILENAMES[target]))
            for target in KNOWN_FORMATS
            if target != source_format
        ]

    def project(self, source_content: str, source_format: str, target_format: str) -> str:
        if target_format == FORMAT_LLMSTXT:
            return self._to_llmstxt(source_content, source_format)
        return self._to_markdown_projection(source_content, source_format, TARGETS[target_format])

    def get_sync_status(self, directory: str | Path) -> dict[str, str]:
        root = Path(directory)
        detected = self.detect_source(root)
        if not detected:
            return {}
        source_fmt, source_path = detected
        source_content = source_path.read_text(encoding="utf-8", errors="replace")
        status: dict[str, str] = {}
        if source_fmt == FORMAT_AGENTKIT_SOURCE:
            status[FORMAT_AGENTKIT_SOURCE] = "source"
        for fmt in KNOWN_FORMATS:
            path = source_path_for_format(root, fmt)
            if fmt == source_fmt:
                status[fmt] = "source"
            elif not path.exists():
                status[fmt] = "missing"
            else:
                derived = path.read_text(encoding="utf-8", errors="replace")
                if _extract_gen_hash(derived) is None:
                    status[fmt] = "unmanaged"
                elif self.is_stale(source_content, derived):
                    status[fmt] = "stale"
                else:
                    status[fmt] = "ok"
        return status

    def is_stale(self, source_content: str, derived_content: str) -> bool:
        embedded = _extract_gen_hash(derived_content)
        if embedded is None:
            return False
        return embedded != _source_hash(source_content)

    def _project_name(self, content: str) -> str:
        for line in content.splitlines():
            if line.startswith("# "):
                title = line[2:].strip()
                title = re.sub(r"^(AGENTS|AGENT)\.md\s+[—-]\s+", "", title, flags=re.IGNORECASE)
                title = re.sub(r"^(AGENTS|AGENT)\.md\s+for\s+", "", title, flags=re.IGNORECASE)
                return title or "Project"
        return "Project"

    def _to_markdown_projection(self, content: str, source_format: str, target: ProjectionTarget) -> str:
        stamp = _make_gen_stamp(content)
        project_name = self._project_name(content)
        intro = _get_top_level_intro(content)
        sections = [s for s in _parse_sections(content) if s.level > 1]
        heading = f"# {project_name}" if target.heading_style == "project" else f"# {target.filename} for {project_name}"
        lines = [heading, "", stamp, ""]
        if intro:
            cleaned_intro = intro.strip()
            if cleaned_intro and cleaned_intro != heading:
                lines.extend([cleaned_intro, ""])
        category_map = {
            "overview": [],
            "commands": [],
            "patterns": [],
            "context": [],
            "other": [],
        }
        for sec in sections:
            title = sec.title.lower()
            if any(k in title for k in ("overview", "goal", "purpose", "about", "project")):
                category_map["overview"].append(sec)
            elif any(k in title for k in ("command", "setup", "install", "test", "build", "run")):
                category_map["commands"].append(sec)
            elif any(k in title for k in ("pattern", "style", "rule", "convention", "format")):
                category_map["patterns"].append(sec)
            elif any(k in title for k in ("memory", "session", "context", "workspace", "safety", "cron", "agent")):
                category_map["context"].append(sec)
            else:
                category_map["other"].append(sec)
        normalized_titles = {
            "overview": "Overview",
            "commands": "Commands",
            "patterns": "Patterns & Conventions",
            "context": "Context & Workspace",
            "other": "Additional Context",
        }
        for key in ("overview", "commands", "patterns", "context", "other"):
            secs = category_map[key]
            if not secs:
                continue
            lines.extend([f"## {normalized_titles[key]}", ""])
            for sec in secs:
                lines.extend([f"### {sec.title}", ""])
                if sec.body:
                    lines.extend([sec.body, ""])
        lines.extend(["## Source Projection", "", f"- Canonical source format: {source_format}", f"- Projected target: {target.filename}", ""])
        return "\n".join(lines).rstrip() + "\n"

    def _to_llmstxt(self, content: str, source_format: str) -> str:
        stamp = _make_gen_stamp(content)
        project_name = self._project_name(content)
        intro = _get_top_level_intro(content)
        sections = [s for s in _parse_sections(content) if s.level > 1]
        description = intro.strip()
        if not description:
            for sec in sections:
                if sec.body:
                    description = sec.body.splitlines()[0].strip()
                    break
        description = re.sub(r"^>\s*", "", description)[:280].strip()
        lines = [f"# {project_name}", "", stamp, ""]
        if description:
            lines.extend([f"> {description}", ""])
        for sec in sections:
            if sec.level != 2 and sec.level != 3:
                continue
            lines.extend([f"## {sec.title}", ""])
            if sec.body:
                lines.extend([textwrap.fill(sec.body.replace("\n", " "), width=80), ""])
        lines.extend(["## Source", "", f"- Generated from: {source_format}", ""])
        return "\n".join(lines).rstrip() + "\n"
