"""Smart context builder — tiered project context for LLM prompts.

Produces three tiers of context:

- **Tier 1 — full code**: files that the current stage creates, modifies, or
  directly depends on.  Sent as complete source.
- **Tier 2 — signatures**: all other project source files.  Sent as a compact
  AST index (class/function/type signatures only).
- **Tier 3 — paths**: the project file tree (already handled separately by
  ``_scan_project_tree``).

Usage::

    builder = SmartContextBuilder(project_root)
    result = builder.build(stage_section=section, plan=plan)
    # result.signatures   — compact AST index string
    # result.full_files   — formatted full source of relevant files
    # result.relevant     — set of relevant file paths
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from pathlib import Path

from skaro_core.context._ast_index import build_project_index
from skaro_core.context._relevance import find_relevant_paths
from skaro_core.phases.base import SKIP_DIRS, SOURCE_EXTENSIONS

log = logging.getLogger(__name__)


@dataclass
class SmartContext:
    """Result of a smart context build."""

    signatures: str = ""
    """Compact AST index of all project source files."""

    full_files: str = ""
    """Formatted full source code of relevant (Tier 1) files."""

    relevant_paths: set[str] = field(default_factory=set)
    """Set of file paths selected as Tier 1 (relevant)."""

    stats: dict[str, int] = field(default_factory=dict)
    """Statistics: total_files, signatures_files, full_files, etc."""


class SmartContextBuilder:
    """Build tiered project context for LLM phases.

    Designed to be instantiated once per phase invocation and reused
    if the same phase needs to build context multiple times (e.g. fix
    conversation that re-reads files on each turn).
    """

    def __init__(
        self,
        project_root: Path,
        *,
        skip_dirs: set[str] | None = None,
        source_extensions: set[str] | None = None,
    ):
        self.root = project_root
        self._skip_dirs = skip_dirs or SKIP_DIRS
        self._extensions = source_extensions or SOURCE_EXTENSIONS

    def build(
        self,
        *,
        stage_section: str = "",
        plan: str = "",
        extra_relevant: set[str] | None = None,
        max_full_files: int = 15,
        max_full_file_size: int = 15_000,
        max_index_files: int = 200,
    ) -> SmartContext:
        """Build tiered context.

        Args:
            stage_section: Current stage description (highest priority for
                relevance extraction).
            plan: Full plan text (used as fallback for relevance).
            extra_relevant: Additional file paths to always include as full code.
            max_full_files: Maximum number of Tier 1 (full code) files.
            max_full_file_size: Truncate individual files above this size (chars).
            max_index_files: Maximum files in the AST index.

        Returns:
            :class:`SmartContext` with signatures, full_files, and stats.
        """
        # ── 1. Determine relevant files ─────────────────────────
        context_text = stage_section or plan
        relevant = find_relevant_paths(
            context_text, self.root,
            expand_imports=True,
            max_depth=1,
        )

        if extra_relevant:
            relevant |= extra_relevant

        # ── 2. Build AST index (all project files) ─────────────
        signatures = build_project_index(
            self.root,
            skip_dirs=self._skip_dirs,
            source_extensions=self._extensions,
            max_files=max_index_files,
        )

        # ── 3. Read full source for Tier 1 files ───────────────
        full_files_dict: dict[str, str] = {}
        # Sort for deterministic order; prioritize files from stage_section
        sorted_relevant = sorted(relevant)

        for fp in sorted_relevant:
            if len(full_files_dict) >= max_full_files:
                break
            abs_path = self.root / fp
            if not abs_path.is_file():
                continue
            if abs_path.suffix.lower() not in self._extensions:
                continue
            # Skip dot-directories but NOT dot-files (.env, .eslintrc)
            parts = abs_path.relative_to(self.root).parts
            if any(p in self._skip_dirs or p.startswith(".") for p in parts[:-1]):
                continue
            try:
                content = abs_path.read_text("utf-8", errors="replace")
            except (OSError, PermissionError):
                continue
            if len(content) > max_full_file_size:
                content = content[:max_full_file_size] + "\n... (truncated)"
            full_files_dict[fp] = content

        full_files_text = _format_full_files(full_files_dict)

        stats = {
            "total_relevant": len(relevant),
            "full_files_sent": len(full_files_dict),
            "index_length_chars": len(signatures),
            "full_files_length_chars": len(full_files_text),
        }

        return SmartContext(
            signatures=signatures,
            full_files=full_files_text,
            relevant_paths=relevant,
            stats=stats,
        )


def _format_full_files(files: dict[str, str]) -> str:
    """Format a dict of {path: content} into markdown code blocks."""
    if not files:
        return ""
    parts = []
    for fpath, content in files.items():
        parts.append(f"### {fpath}\n```\n{content}\n```")
    return "\n\n".join(parts)
