"""
Check Command - CI/CD Gate for Pre-Merge Impact Analysis.

This module implements the logic to verify changes against the dependency graph.
It acts as the primary gatekeeper in CI/CD pipelines.
"""

import logging
import re
import subprocess
import sys
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, List, Tuple

import click
from pydantic import BaseModel, Field
from rich.console import Console

from ...analysis.blast_radius import BlastRadiusAnalyzer
from ...core.stitching import Stitcher
from ...core.storage.sqlite import SQLiteStorage
from ...core.types import NodeType
from ...parsing.engine import ScanConfig, create_default_engine
from ..renderers import JsonRenderer
from ..utils import load_graph

logger = logging.getLogger(__name__)
console = Console(stderr=True)  # Use stderr for logs to not pollute JSON output pipes


# --- API Models (External Contract) ---
class CheckResultStatus(str, Enum):
    PASS = "PASS"
    BLOCKED = "BLOCKED"
    WARN = "WARN"


class ApiChangedFile(BaseModel):
    path: str
    change_type: str


class ApiViolation(BaseModel):
    rule: str
    severity: str
    message: str


class CheckResponse(BaseModel):
    """
    Standardized response for check command.
    """

    result: CheckResultStatus
    exit_code: int
    changed_files_count: int
    critical_count: int
    high_count: int
    violations: List[ApiViolation] = Field(default_factory=list)
    details_url: str | None = None


# --- Internal Domain Models ---
class CheckResult(Enum):
    PASS = 0
    BLOCKED = 1
    WARN = 2


@dataclass
class ChangedFile:
    path: str
    change_type: str  # 'A', 'M', 'D', etc.
    old_path: str | None = None


@dataclass
class CheckReport:
    """Internal report generated by the analysis engine."""

    result: CheckResult = CheckResult.PASS
    changed_files: List[ChangedFile] = field(default_factory=list)
    critical_count: int = 0
    high_count: int = 0
    violations: List[Any] = field(default_factory=list)


class CheckEngine:
    """
    Performs risk assessment on changed files using the dependency graph.
    """

    def __init__(self, graph_path: str = ".jnkn/jnkn.db"):
        self.graph_path = graph_path
        self.graph = load_graph(graph_path)

    def ensure_graph_exists(self):
        """
        Auto-scan capability: If graph is missing, build it on the fly.
        This enables the 'init -> check' workflow without an explicit 'scan' step.
        """
        if self.graph is not None and self.graph.node_count > 0:
            return

        console.print("[dim]Graph not found or empty. Running auto-scan...[/dim]")

        # Setup paths
        db_path = Path(self.graph_path)
        root_dir = Path.cwd()

        # Run scan
        engine = create_default_engine()
        storage = SQLiteStorage(db_path)
        storage.clear()

        config = ScanConfig(root_dir=root_dir, incremental=False)
        result = engine.scan_and_store(storage, config)

        if result.is_err():
            console.print(f"[red]Auto-scan failed:[/red] {result.unwrap_err().message}")
            return

        # Run stitcher
        graph = storage.load_graph()
        stitcher = Stitcher()
        new_edges = stitcher.stitch(graph)
        storage.save_edges_batch(new_edges)

        # Reload graph
        self.graph = storage.load_graph()
        storage.close()
        console.print(f"[dim]Auto-scan complete. Found {self.graph.node_count} nodes.[/dim]")

    def analyze(self, changes: List[ChangedFile]) -> CheckReport:
        # Ensure we have data to analyze
        self.ensure_graph_exists()

        report = CheckReport(changed_files=changes)

        # If no graph (even after auto-scan attempt), fallback to static
        if not self.graph:
            self._analyze_static(changes, report)
            return report

        analyzer = BlastRadiusAnalyzer(self.graph)

        for file in changes:
            # 1. Resolve file to graph node
            # We use the file:// prefix convention
            node_id = f"file://{file.path}"

            # If the file itself isn't a node (e.g. unknown type), skip deep analysis
            if not self.graph.has_node(node_id):
                self._analyze_single_file_heuristic(file, report)
                continue

            # 2. Calculate Impact
            # We check if this file affects anything downstream
            impact = analyzer.calculate([node_id])
            impact_count = impact.get("count", 0)

            # 3. Apply Rules
            # Rule: High Blast Radius
            if impact_count > 20:
                report.violations.append(
                    ApiViolation(
                        rule="HIGH_BLAST_RADIUS",
                        severity="high",
                        message=f"{file.path} impacts {impact_count} downstream artifacts.",
                    )
                )
                report.high_count += 1

            # Rule: Critical Node Modification
            # Check if this node provides for critical infrastructure
            downstream = self.graph.get_descendants(node_id)
            for target_id in downstream:
                target = self.graph.get_node(target_id)
                if target and target.type == NodeType.INFRA_RESOURCE:
                    report.violations.append(
                        ApiViolation(
                            rule="INFRA_IMPACT",
                            severity="critical",
                            message=f"Change to {file.path} impacts infrastructure {target.name}",
                        )
                    )
                    report.critical_count += 1
                    break  # One critical violation is enough to flag the file

        return report

    def _analyze_static(self, changes: List[ChangedFile], report: CheckReport):
        """Fallback analysis when no graph is available."""
        for file in changes:
            self._analyze_single_file_heuristic(file, report)

    def _analyze_single_file_heuristic(self, file: ChangedFile, report: CheckReport):
        """Heuristic checks based purely on filenames."""
        path = file.path.lower()

        # Critical: Infrastructure Definitions
        if path.endswith(".tf") or "terraform" in path:
            report.violations.append(
                ApiViolation(
                    rule="INFRA_CHANGE",
                    severity="critical",
                    message=f"Infrastructure change detected: {file.path}",
                )
            )
            report.critical_count += 1

        # Critical: Data Schema
        elif "migrations" in path or "schema" in path or path.endswith(".sql"):
            report.violations.append(
                ApiViolation(
                    rule="SCHEMA_CHANGE",
                    severity="high",
                    message=f"Data schema change detected: {file.path}",
                )
            )
            report.high_count += 1


# --- Git Utilities ---


def get_changed_files_from_git(base_ref: str, head_ref: str) -> List[ChangedFile]:
    """
    Get changed files between two git refs using git diff.
    """
    try:
        # Run git diff --name-status
        result = subprocess.run(
            ["git", "diff", "--name-status", base_ref, head_ref],
            capture_output=True,
            text=True,
            check=True,
        )
    except subprocess.CalledProcessError as e:
        raise RuntimeError(f"Git command failed: {e.stderr}")

    changes = []
    for line in result.stdout.splitlines():
        if not line.strip():
            continue

        parts = line.split("\t")
        status = parts[0][0]  # First char (M, A, D, R)

        if status == "R":  # Rename
            if len(parts) >= 3:
                old_path = parts[1]
                new_path = parts[2]
                changes.append(ChangedFile(path=new_path, change_type="RENAME", old_path=old_path))
        else:
            path = parts[1]
            changes.append(ChangedFile(path=path, change_type=status))

    return changes


def get_changed_files_from_diff_file(diff_path: str) -> List[ChangedFile]:
    """
    Parse a standard Unified Diff file to find changed filenames.
    """
    changes = []
    path = Path(diff_path)
    if not path.exists():
        raise FileNotFoundError(f"Diff file not found: {diff_path}")

    content = path.read_text()
    diff_pattern = re.compile(r"^diff --git a/(.*?) b/(.*?)$", re.MULTILINE)

    for match in diff_pattern.finditer(content):
        file_path = match.group(2)
        changes.append(ChangedFile(path=file_path, change_type="MODIFIED"))

    return changes


class _null_context:
    """Helper for non-capture mode."""

    def __enter__(self):
        pass

    def __exit__(self, *args):
        pass


# =============================================================================
# CLI Command
# =============================================================================


@click.command()
@click.option(
    "--diff", "diff_file", type=click.Path(exists=True), help="Path to a unified diff file"
)
@click.option("--git-diff", "git_diff", nargs=2, help="Git refs to compare (BASE HEAD)")
@click.option(
    "--fail-if-critical", is_flag=True, help="Exit with error if critical violations found"
)
@click.option("--json", "as_json", is_flag=True, help="Output as JSON (Standard Envelope)")
@click.option("--format", "output_format", type=click.Choice(["text", "markdown"]), default="text")
@click.option("--quiet", "-q", is_flag=True, help="Suppress text output")
def check(
    diff_file: str | None,
    git_diff: Tuple[str, str] | None,
    fail_if_critical: bool,
    as_json: bool,
    output_format: str,
    quiet: bool,
):
    """
    Run pre-merge impact analysis.

    Compares changes against the dependency graph to detect breaking changes.
    Will automatically scan the codebase if no graph exists.
    """
    renderer = JsonRenderer("check")

    # Context handling: Capture stdout/stderr only if JSON output is requested
    context_manager = renderer.capture() if as_json else _null_context()

    error_to_report = None
    api_response = None

    with context_manager:
        try:
            # 1. Acquire Changed Files
            changed_files: List[ChangedFile] = []

            if diff_file:
                changed_files = get_changed_files_from_diff_file(diff_file)
            elif git_diff:
                base, head = git_diff
                changed_files = get_changed_files_from_git(base, head)
            else:
                # Fallback: Try to compare HEAD against previous commit if nothing specified
                try:
                    changed_files = get_changed_files_from_git("HEAD~1", "HEAD")
                except Exception:
                    changed_files = []

            # 2. Run Analysis Engine
            engine = CheckEngine()
            report = engine.analyze(changed_files)

            # 3. Apply Failure Policy
            if fail_if_critical and report.critical_count > 0:
                report.result = CheckResult.BLOCKED
            elif report.high_count > 5:
                report.result = CheckResult.WARN

            # 4. Map to API Model
            api_violations = [
                ApiViolation(rule=v.rule, severity=v.severity, message=v.message)
                for v in report.violations
            ]

            api_response = CheckResponse(
                result=CheckResultStatus[report.result.name],
                exit_code=report.result.value,
                changed_files_count=len(report.changed_files),
                critical_count=report.critical_count,
                high_count=report.high_count,
                violations=api_violations,
            )

        except Exception as e:
            error_to_report = e

    # Render Output
    if as_json:
        if error_to_report:
            renderer.render_error(error_to_report)
            sys.exit(1)
        elif api_response:
            renderer.render_success(api_response)
            sys.exit(api_response.exit_code)
    else:
        # Legacy Text Output
        if error_to_report:
            if not quiet:
                console.print(f"❌ [red]Error:[/red] {error_to_report}")
            sys.exit(1)

        if api_response:
            color = "green"
            if api_response.result == CheckResultStatus.BLOCKED:
                color = "red"
            elif api_response.result == CheckResultStatus.WARN:
                color = "yellow"

            if not quiet:
                console.print(
                    f"\n[bold]Analysis Complete:[/bold] {len(api_response.violations)} violations found."
                )
                for v in api_response.violations:
                    icon = (
                        "🔴" if v.severity == "critical" else "🟠" if v.severity == "high" else "⚪"
                    )
                    console.print(f"  {icon} [{v.severity.upper()}] {v.message}")

                console.print(f"\nResult: [bold {color}]{api_response.result.value}[/bold {color}]")

            sys.exit(api_response.exit_code)

        sys.exit(1)
