"""Context writers — write learned patterns to agent-specific context files.

Writers take Recommendations and write them to the appropriate context
injection mechanism for each agent system (CLAUDE.md, .cursorrules, etc.).
"""

from __future__ import annotations

import re
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from pathlib import Path

from .models import (
    ProjectInfo,
    Recommendation,
    RecommendationTarget,
)

# Marker delimiters for Headroom-managed sections
_MARKER_START = "<!-- headroom:learn:start -->"
_MARKER_END = "<!-- headroom:learn:end -->"
_MARKER_PATTERN = re.compile(
    re.escape(_MARKER_START) + r".*?" + re.escape(_MARKER_END),
    re.DOTALL,
)


# =============================================================================
# Abstract Writer
# =============================================================================


class ContextWriter(ABC):
    """Base class for writing recommendations to context/memory files."""

    @abstractmethod
    def write(
        self,
        recommendations: list[Recommendation],
        project: ProjectInfo,
        dry_run: bool = True,
    ) -> WriteResult: ...


# =============================================================================
# Write Result
# =============================================================================


class WriteResult:
    """Result of a write operation."""

    def __init__(self) -> None:
        self.files_written: list[Path] = []
        self.content_by_file: dict[Path, str] = {}
        self.dry_run: bool = True

    def add(self, path: Path, content: str) -> None:
        self.files_written.append(path)
        self.content_by_file[path] = content


# =============================================================================
# Shared section builder
# =============================================================================


def _build_section(recommendations: list[Recommendation]) -> str:
    """Build the marker-delimited section content from recommendations."""
    now = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    lines = [
        _MARKER_START,
        "## Headroom Learned Patterns",
        f"*Auto-generated by `headroom learn` on {now} — do not edit manually*",
        "",
    ]

    for rec in recommendations:
        lines.append(f"### {rec.section}")
        if rec.estimated_tokens_saved > 0:
            lines.append(f"*~{rec.estimated_tokens_saved:,} tokens/session saved*")
        lines.append(rec.content)
        lines.append("")

    lines.append(_MARKER_END)
    return "\n".join(lines)


# Matches the "*~N tokens/session saved*" annotation emitted by _build_section.
_TOKENS_ANNOTATION_PATTERN = re.compile(r"\*~([\d,]+) tokens/session saved\*\n?")


def extract_marker_block(file_content: str) -> str | None:
    """Return the raw text of the headroom:learn marker block, or None.

    Unlike _parse_prior_recommendations, this returns the block verbatim
    (including the start/end markers) so it can be fed back to an LLM as
    context without losing formatting. Returns None if no block is present.
    """
    match = _MARKER_PATTERN.search(file_content)
    return match.group(0) if match else None


def _parse_prior_recommendations(existing: str) -> list[Recommendation]:
    """Parse recommendations out of a prior marker block.

    Returns [] if no marker block is present or it contains no sections.
    The returned Recommendation objects are round-trip compatible with
    _build_section — target is set to a placeholder since the marker block
    itself doesn't record it (blocks are always per-file and per-target).
    """
    match = _MARKER_PATTERN.search(existing)
    if not match:
        return []
    inner = match.group(0)[len(_MARKER_START) : -len(_MARKER_END)]

    recs: list[Recommendation] = []
    for part in re.split(r"\n### ", "\n" + inner)[1:]:
        heading_line, _, body = part.partition("\n")
        heading = heading_line.strip()
        if not heading:
            continue

        tokens_saved = 0
        tokens_match = _TOKENS_ANNOTATION_PATTERN.match(body)
        if tokens_match:
            tokens_saved = int(tokens_match.group(1).replace(",", ""))
            body = body[tokens_match.end() :]

        recs.append(
            Recommendation(
                target=RecommendationTarget.CONTEXT_FILE,
                section=heading,
                content=body.rstrip(),
                estimated_tokens_saved=tokens_saved,
            )
        )
    return recs


def _merge_recommendations(
    file_path: Path,
    new_recommendations: list[Recommendation],
) -> list[Recommendation]:
    """Union new recommendations with prior ones whose section is not re-surfaced.

    Sections produced by the current run take precedence over same-named
    prior sections — the latest analysis is authoritative. Prior sections
    whose headings do not reappear in the new run are carried forward so
    a re-run doesn't silently drop accumulated learnings. To fully rebuild
    the block, delete it manually and re-run.
    """
    if not file_path.exists():
        return new_recommendations
    prior = _parse_prior_recommendations(file_path.read_text(encoding="utf-8"))
    if not prior:
        return new_recommendations
    new_sections = {r.section for r in new_recommendations}
    carried = [p for p in prior if p.section not in new_sections]
    return list(new_recommendations) + carried


def _merge_into_file(file_path: Path, new_recommendations: list[Recommendation]) -> str:
    """Merge new recommendations with any existing marker block and rebuild the file."""
    merged = _merge_recommendations(file_path, new_recommendations)
    section = _build_section(merged)
    if file_path.exists():
        existing = file_path.read_text(encoding="utf-8")
        if _MARKER_START in existing:
            return _MARKER_PATTERN.sub(lambda _match: section, existing)
        return existing.rstrip() + "\n\n" + section + "\n"
    return section + "\n"


# =============================================================================
# Claude Code Writer
# =============================================================================


class ClaudeCodeWriter(ContextWriter):
    """Writes learned patterns to CLAUDE.md and MEMORY.md for Claude Code."""

    def write(
        self,
        recommendations: list[Recommendation],
        project: ProjectInfo,
        dry_run: bool = True,
    ) -> WriteResult:
        result = WriteResult()
        result.dry_run = dry_run

        context_recs = [r for r in recommendations if r.target == RecommendationTarget.CONTEXT_FILE]
        memory_recs = [r for r in recommendations if r.target == RecommendationTarget.MEMORY_FILE]

        if context_recs:
            claude_md_path = self._resolve_context_path(project)
            full_content = _merge_into_file(claude_md_path, context_recs)
            result.add(claude_md_path, full_content)
            if not dry_run:
                claude_md_path.parent.mkdir(parents=True, exist_ok=True)
                claude_md_path.write_text(full_content, encoding="utf-8")

        if memory_recs:
            memory_path = self._resolve_memory_path(project)
            full_content = _merge_into_file(memory_path, memory_recs)
            result.add(memory_path, full_content)
            if not dry_run:
                memory_path.parent.mkdir(parents=True, exist_ok=True)
                memory_path.write_text(full_content, encoding="utf-8")

        return result

    def _resolve_context_path(self, project: ProjectInfo) -> Path:
        if project.context_file:
            return project.context_file
        # If project path is the home directory, write to ~/.claude/CLAUDE.md
        # (the global location Claude Code reads) instead of ~/CLAUDE.md
        if project.project_path == Path.home():
            return Path.home() / ".claude" / "CLAUDE.md"
        return project.project_path / "CLAUDE.md"

    def _resolve_memory_path(self, project: ProjectInfo) -> Path:
        if project.memory_file:
            return project.memory_file
        return project.data_path / "memory" / "MEMORY.md"


# =============================================================================
# Codex Writer (OpenAI Codex CLI)
# =============================================================================


class CodexWriter(ContextWriter):
    """Writes learned patterns to AGENTS.md and instructions.md for Codex CLI."""

    def write(
        self,
        recommendations: list[Recommendation],
        project: ProjectInfo,
        dry_run: bool = True,
    ) -> WriteResult:
        result = WriteResult()
        result.dry_run = dry_run

        context_recs = [r for r in recommendations if r.target == RecommendationTarget.CONTEXT_FILE]
        memory_recs = [r for r in recommendations if r.target == RecommendationTarget.MEMORY_FILE]

        if context_recs:
            agents_md = project.context_file or (project.project_path / "AGENTS.md")
            full_content = _merge_into_file(agents_md, context_recs)
            result.add(agents_md, full_content)
            if not dry_run:
                agents_md.parent.mkdir(parents=True, exist_ok=True)
                agents_md.write_text(full_content, encoding="utf-8")

        if memory_recs:
            instructions_md = project.memory_file or (project.data_path.parent / "instructions.md")
            full_content = _merge_into_file(instructions_md, memory_recs)
            result.add(instructions_md, full_content)
            if not dry_run:
                instructions_md.parent.mkdir(parents=True, exist_ok=True)
                instructions_md.write_text(full_content, encoding="utf-8")

        return result


# =============================================================================
# Gemini Writer (Google Gemini CLI)
# =============================================================================


class GeminiWriter(ContextWriter):
    """Writes learned patterns to GEMINI.md for Gemini CLI."""

    def write(
        self,
        recommendations: list[Recommendation],
        project: ProjectInfo,
        dry_run: bool = True,
    ) -> WriteResult:
        result = WriteResult()
        result.dry_run = dry_run

        if not recommendations:
            return result

        gemini_md = project.context_file or (project.project_path / "GEMINI.md")
        full_content = _merge_into_file(gemini_md, recommendations)
        result.add(gemini_md, full_content)
        if not dry_run:
            gemini_md.parent.mkdir(parents=True, exist_ok=True)
            gemini_md.write_text(full_content, encoding="utf-8")

        return result
