"""Living spec tracking and drift detection for Spec-Driven Development."""

import hashlib
import json
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


@dataclass
class SpecVersion:
    """Represents a tracked version of a specification."""

    spec_id: str
    version: str
    content_hash: str  # SHA-256 of spec content
    updated_at: str  # ISO 8601 timestamp
    linked_code_paths: list[str] = field(default_factory=list)


@dataclass
class DriftResult:
    """Result of drift detection between spec and code."""

    has_drift: bool
    spec_changed_since_code: bool
    code_changed_since_spec: bool
    details: str


class SpecTracker:
    """Tracks specifications and detects drift from implementation."""

    def __init__(self, tracking_dir: Path | None = None):
        """Initialize the spec tracker.

        Args:
            tracking_dir: Directory for tracking metadata. Defaults to .abi/
        """
        self.tracking_dir = tracking_dir or Path(".abi")
        self.tracking_file = self.tracking_dir / "spec_tracking.json"
        self._ensure_tracking_dir()

    def _ensure_tracking_dir(self) -> None:
        """Ensure the tracking directory exists."""
        self.tracking_dir.mkdir(parents=True, exist_ok=True)

    def _compute_content_hash(self, file_path: Path) -> str:
        """Compute SHA-256 hash of file content.

        Args:
            file_path: Path to the file

        Returns:
            Hexadecimal SHA-256 hash string
        """
        sha256 = hashlib.sha256()
        with file_path.open("rb") as f:
            # Read in chunks to handle large files
            for chunk in iter(lambda: f.read(8192), b""):
                sha256.update(chunk)
        return sha256.hexdigest()

    def _load_tracking_data(self) -> dict[str, Any]:
        """Load tracking data from JSON file.

        Returns:
            Dictionary of tracked specs
        """
        if not self.tracking_file.exists():
            return {}

        with self.tracking_file.open("r", encoding="utf-8") as f:
            return json.load(f)

    def _save_tracking_data(self, data: dict[str, Any]) -> None:
        """Save tracking data to JSON file.

        Args:
            data: Dictionary of tracked specs to save
        """
        with self.tracking_file.open("w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)

    def _get_latest_mtime(self, paths: list[str]) -> float:
        """Get the latest modification time from a list of file paths.

        Args:
            paths: List of file or directory paths

        Returns:
            Latest modification timestamp, or 0 if no files found
        """
        latest_mtime = 0.0

        for path_str in paths:
            path = Path(path_str)
            if not path.exists():
                continue

            if path.is_file():
                latest_mtime = max(latest_mtime, path.stat().st_mtime)
            elif path.is_dir():
                # Recursively check all files in directory
                for file_path in path.rglob("*"):
                    if file_path.is_file():
                        latest_mtime = max(latest_mtime, file_path.stat().st_mtime)

        return latest_mtime

    def track(self, spec_path: str, code_paths: list[str]) -> SpecVersion:
        """Track a specification and its linked code paths.

        Args:
            spec_path: Path to the specification file
            code_paths: List of code file/directory paths linked to this spec

        Returns:
            SpecVersion object representing the tracked spec

        Raises:
            FileNotFoundError: If spec_path does not exist
        """
        spec_file = Path(spec_path)
        if not spec_file.exists():
            raise FileNotFoundError(f"Spec file not found: {spec_path}")

        # Compute content hash
        content_hash = self._compute_content_hash(spec_file)

        # Create spec version
        spec_version = SpecVersion(
            spec_id=spec_path,
            version=datetime.now(timezone.utc).isoformat(),
            content_hash=content_hash,
            updated_at=datetime.now(timezone.utc).isoformat(),
            linked_code_paths=code_paths,
        )

        # Load existing tracking data
        tracking_data = self._load_tracking_data()

        # Update tracking data
        tracking_data[spec_path] = asdict(spec_version)

        # Save tracking data
        self._save_tracking_data(tracking_data)

        return spec_version

    def detect_drift(self, spec_path: str, code_paths: list[str]) -> DriftResult:
        """Detect drift between specification and code.

        Uses both time-based and hash-based detection:
        1. Compare file modification times
        2. Compare content hashes with tracked versions

        Args:
            spec_path: Path to the specification file
            code_paths: List of code file/directory paths to check

        Returns:
            DriftResult indicating drift status and details

        Raises:
            FileNotFoundError: If spec_path does not exist
        """
        spec_file = Path(spec_path)
        if not spec_file.exists():
            raise FileNotFoundError(f"Spec file not found: {spec_path}")

        # Get modification times
        spec_mtime = spec_file.stat().st_mtime
        code_mtime = self._get_latest_mtime(code_paths)

        # Time-based detection
        spec_newer_than_code = spec_mtime > code_mtime if code_mtime > 0 else False
        code_newer_than_spec = code_mtime > spec_mtime if code_mtime > 0 else False

        # Hash-based detection
        tracking_data = self._load_tracking_data()
        current_hash = self._compute_content_hash(spec_file)

        spec_hash_changed = False
        tracked_spec_mtime = None
        if spec_path in tracking_data:
            tracked_hash = tracking_data[spec_path].get("content_hash", "")
            spec_hash_changed = current_hash != tracked_hash
            # Get the timestamp when spec was last tracked
            tracked_updated_at = tracking_data[spec_path].get("updated_at", "")
            if tracked_updated_at:
                from datetime import datetime
                tracked_dt = datetime.fromisoformat(tracked_updated_at)
                tracked_spec_mtime = tracked_dt.timestamp()

        # Code drift should be measured against when the spec was last tracked,
        # not merely whether the code file was newer than the spec file.
        code_changed_since_tracking = False
        if tracked_spec_mtime and code_mtime > 0:
            code_changed_since_tracking = code_mtime > tracked_spec_mtime

        # Determine drift status and details
        has_drift = False
        details_parts = []

        # Check if both changed since last tracking
        both_changed = False
        if tracked_spec_mtime and code_mtime > 0:
            spec_changed_since_tracking = spec_hash_changed
            both_changed = spec_changed_since_tracking and code_changed_since_tracking

        if both_changed:
            has_drift = True
            details_parts.append(
                "Both spec and code changed independently — "
                "manual reconciliation needed"
            )
        elif spec_newer_than_code and spec_hash_changed:
            has_drift = True
            details_parts.append(
                "Spec updated — code may need regeneration (spec is newer than code)"
            )
        elif code_changed_since_tracking and spec_path in tracking_data:
            has_drift = True
            details_parts.append(
                "Code updated without spec update — drift detected "
                "(code is newer than spec)"
            )

        if not has_drift:
            if spec_path not in tracking_data:
                details_parts.append("Spec not yet tracked — use 'track' to begin")
            elif not code_paths or code_mtime == 0:
                details_parts.append("No code files found to compare")
            else:
                details_parts.append("Spec and code are in sync")

        details = "; ".join(details_parts) if details_parts else "No drift detected"

        return DriftResult(
            has_drift=has_drift,
            spec_changed_since_code=spec_newer_than_code and spec_hash_changed,
            code_changed_since_spec=code_changed_since_tracking
            and spec_path in tracking_data,
            details=details,
        )
