"""Metrics module - feedback loop for complexity assessment calibration.

This module provides the infrastructure for tracking assessment accuracy
and calibrating complexity predictions over time.

The feedback loop:
1. Record predictions at assessment time
2. Capture actual effort from task results
3. Calculate accuracy metrics
4. Generate calibration recommendations

Metrics are stored per-workspace in metrics.yaml and can be aggregated
across projects for global calibration.
"""

from __future__ import annotations

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

from cleave.core.file_utils import atomic_write_text
from cleave.core.yaml_utils import parse_yaml_simple, to_yaml

logger = logging.getLogger(__name__)


# =============================================================================
# DATA CLASSES
# =============================================================================


@dataclass
class AssessmentRecord:
    """Record of a single assessment with outcome tracking."""

    directive_hash: str  # SHA256 hash of directive (privacy)
    timestamp: str
    predicted_complexity: float
    predicted_decision: str  # execute or cleave
    pattern_matched: str | None
    pattern_confidence: float
    systems: int
    modifiers: list[str]
    # Outcome fields (populated after execution)
    actual_effort: str | None = None  # low, medium, high, or None
    actual_success: bool | None = None
    execution_notes: str | None = None
    accuracy_score: float | None = None  # 0.0-1.0


@dataclass
class MetricsSummary:
    """Aggregated metrics across assessments."""

    total_assessments: int = 0
    assessments_with_outcomes: int = 0
    average_accuracy: float = 0.0
    pattern_accuracy: dict[str, float] = field(default_factory=dict)
    complexity_calibration: dict[str, float] = field(default_factory=dict)
    recommendations: list[str] = field(default_factory=list)


# =============================================================================
# EFFORT MAPPING
# =============================================================================

# Map result status/content to effort level
EFFORT_INDICATORS = {
    "low": [
        "trivial",
        "straightforward",
        "simple fix",
        "quick change",
        "minor",
        "small",
    ],
    "medium": [
        "moderate",
        "several files",
        "required investigation",
        "some complexity",
    ],
    "high": [
        "significant",
        "complex",
        "challenging",
        "extensive",
        "major refactor",
        "many files",
        "unexpected",
    ],
}


def infer_effort_from_result(result_text: str) -> str | None:
    """Infer effort level from task result summary.

    Args:
        result_text: The result/summary section from a task file

    Returns:
        'low', 'medium', 'high', or None if unclear
    """
    result_lower = result_text.lower()

    # Check for explicit effort indicators
    for effort, indicators in EFFORT_INDICATORS.items():
        for indicator in indicators:
            if indicator in result_lower:
                return effort

    # Heuristics based on result length and content
    word_count = len(result_text.split())
    if word_count < 50:
        return "low"
    elif word_count < 200:
        return "medium"
    else:
        return "high"


# =============================================================================
# ACCURACY CALCULATION
# =============================================================================

# Effort to complexity mapping for accuracy calculation
EFFORT_TO_COMPLEXITY = {
    "low": (0.0, 2.5),  # Expected complexity range
    "medium": (2.0, 5.0),
    "high": (4.0, 100.0),
}


def calculate_accuracy(predicted_complexity: float, actual_effort: str) -> float:
    """Calculate accuracy score for a prediction.

    Args:
        predicted_complexity: The complexity score predicted at assessment
        actual_effort: The actual effort level observed

    Returns:
        Accuracy score from 0.0 (wrong) to 1.0 (correct)
    """
    if actual_effort not in EFFORT_TO_COMPLEXITY:
        return 0.5  # Unknown effort, neutral score

    min_expected, max_expected = EFFORT_TO_COMPLEXITY[actual_effort]

    if min_expected <= predicted_complexity <= max_expected:
        return 1.0  # Perfect match

    # Calculate distance-based score
    if predicted_complexity < min_expected:
        distance = min_expected - predicted_complexity
    else:
        distance = predicted_complexity - max_expected

    # Decay accuracy based on distance (max penalty at distance 5)
    accuracy = max(0.0, 1.0 - (distance / 5.0))
    return round(accuracy, 2)


# =============================================================================
# RECORD MANAGEMENT
# =============================================================================


def hash_directive(directive: str) -> str:
    """Create a privacy-preserving hash of a directive.

    Args:
        directive: The original directive text

    Returns:
        First 16 chars of SHA256 hash
    """
    return hashlib.sha256(directive.encode()).hexdigest()[:16]


def record_assessment(
    workspace_path: str | Path,
    directive: str,
    complexity: float,
    decision: str,
    pattern: str | None,
    confidence: float,
    systems: int,
    modifiers: list[str],
) -> AssessmentRecord:
    """Record an assessment for later accuracy tracking.

    Args:
        workspace_path: Path to workspace (for metrics.yaml)
        directive: Original directive text
        complexity: Predicted complexity score
        decision: 'execute' or 'cleave'
        pattern: Matched pattern name or None
        confidence: Pattern match confidence
        systems: Number of systems identified
        modifiers: List of modifier names

    Returns:
        The created AssessmentRecord
    """
    workspace = Path(workspace_path)
    metrics_path = workspace / "metrics.yaml"

    record = AssessmentRecord(
        directive_hash=hash_directive(directive),
        timestamp=datetime.now().isoformat(),
        predicted_complexity=complexity,
        predicted_decision=decision,
        pattern_matched=pattern,
        pattern_confidence=confidence,
        systems=systems,
        modifiers=modifiers,
    )

    # Load existing metrics or create new
    if metrics_path.exists():
        metrics = parse_yaml_simple(metrics_path.read_text())
    else:
        metrics = {"telemetry": {}, "assessments": []}

    # Ensure assessments list exists
    if "assessments" not in metrics:
        metrics["assessments"] = []

    # Add record
    metrics["assessments"].append(
        {
            "directive_hash": record.directive_hash,
            "timestamp": record.timestamp,
            "predicted_complexity": record.predicted_complexity,
            "predicted_decision": record.predicted_decision,
            "pattern_matched": record.pattern_matched,
            "pattern_confidence": record.pattern_confidence,
            "systems": record.systems,
            "modifiers": record.modifiers,
            "actual_effort": None,
            "actual_success": None,
            "accuracy_score": None,
        }
    )

    atomic_write_text(metrics_path, to_yaml(metrics))
    return record


def update_assessment_outcome(
    workspace_path: str | Path,
    directive_hash: str,
    actual_effort: str,
    actual_success: bool,
    execution_notes: str | None = None,
) -> float | None:
    """Update an assessment record with actual outcome.

    Args:
        workspace_path: Path to workspace
        directive_hash: Hash of the original directive
        actual_effort: 'low', 'medium', or 'high'
        actual_success: Whether the task succeeded
        execution_notes: Optional notes about execution

    Returns:
        Accuracy score or None if record not found
    """
    workspace = Path(workspace_path)
    metrics_path = workspace / "metrics.yaml"

    if not metrics_path.exists():
        logger.warning(f"No metrics file found at {metrics_path}")
        return None

    metrics = parse_yaml_simple(metrics_path.read_text())
    assessments = metrics.get("assessments", [])

    for assessment in assessments:
        if assessment.get("directive_hash") == directive_hash:
            predicted_complexity = assessment.get("predicted_complexity", 2.0)
            accuracy = calculate_accuracy(predicted_complexity, actual_effort)

            assessment["actual_effort"] = actual_effort
            assessment["actual_success"] = actual_success
            assessment["execution_notes"] = execution_notes
            assessment["accuracy_score"] = accuracy

            atomic_write_text(metrics_path, to_yaml(metrics))
            return accuracy

    logger.warning(f"Assessment record not found for hash {directive_hash}")
    return None


# =============================================================================
# METRICS SUMMARY
# =============================================================================


def get_metrics_summary(workspace_path: str | Path = ".") -> dict:
    """Get aggregated metrics summary.

    Args:
        workspace_path: Path to workspace or project root

    Returns:
        Dict with metrics summary suitable for JSON output
    """
    workspace = Path(workspace_path)

    # Look for metrics.yaml in workspace or .cleave subdirectory
    metrics_paths = [
        workspace / "metrics.yaml",
        workspace / ".cleave" / "metrics.yaml",
    ]

    metrics = None
    for mp in metrics_paths:
        if mp.exists():
            metrics = parse_yaml_simple(mp.read_text())
            break

    if not metrics:
        return {
            "total_assessments": 0,
            "assessments_with_outcomes": 0,
            "average_accuracy": 0.0,
            "pattern_accuracy": {},
            "recommendations": ["No metrics data found. Run assessments to start collecting data."],
            "assessments": [],
        }

    assessments = metrics.get("assessments", [])

    if not assessments:
        return {
            "total_assessments": 0,
            "assessments_with_outcomes": 0,
            "average_accuracy": 0.0,
            "pattern_accuracy": {},
            "recommendations": ["No assessments recorded yet."],
            "assessments": [],
        }

    # Calculate summary statistics
    total = len(assessments)
    with_outcomes = [a for a in assessments if a.get("accuracy_score") is not None]
    outcomes_count = len(with_outcomes)

    # Average accuracy
    if with_outcomes:
        avg_accuracy = sum(a["accuracy_score"] for a in with_outcomes) / outcomes_count
    else:
        avg_accuracy = 0.0

    # Pattern-specific accuracy
    pattern_accuracy = {}
    pattern_counts = {}
    for a in with_outcomes:
        pattern = a.get("pattern_matched") or "no_pattern"
        if pattern not in pattern_accuracy:
            pattern_accuracy[pattern] = 0.0
            pattern_counts[pattern] = 0
        pattern_accuracy[pattern] += a["accuracy_score"]
        pattern_counts[pattern] += 1

    for pattern in pattern_accuracy:
        if pattern_counts[pattern] > 0:
            pattern_accuracy[pattern] = round(
                pattern_accuracy[pattern] / pattern_counts[pattern], 2
            )

    # Generate recommendations
    recommendations = []

    if outcomes_count < 5:
        recommendations.append(
            f"Only {outcomes_count} outcomes recorded. Need 5+ for reliable calibration."
        )

    if avg_accuracy < 0.6:
        recommendations.append(
            "Average accuracy below 60%. Consider adjusting complexity formula weights."
        )

    for pattern, accuracy in pattern_accuracy.items():
        if accuracy < 0.5 and pattern_counts.get(pattern, 0) >= 3:
            recommendations.append(
                f"Pattern '{pattern}' has low accuracy ({accuracy:.0%}). "
                f"Review systems_base and modifiers_default for this pattern."
            )

    if not recommendations:
        recommendations.append("Calibration looks good. Continue collecting data.")

    return {
        "total_assessments": total,
        "assessments_with_outcomes": outcomes_count,
        "average_accuracy": round(avg_accuracy, 2),
        "pattern_accuracy": pattern_accuracy,
        "recommendations": recommendations,
        "assessments": assessments[-10:],  # Last 10 for display
    }


# =============================================================================
# AUTO-EXTRACTION FROM TASK RESULTS
# =============================================================================


def extract_outcomes_from_tasks(workspace_path: str | Path) -> list[dict]:
    """Extract outcomes from task result files.

    Scans task files in a workspace and attempts to infer effort/success
    from the result sections.

    Args:
        workspace_path: Path to cleave workspace

    Returns:
        List of extracted outcomes
    """
    workspace = Path(workspace_path)
    outcomes = []

    # Find all task files
    task_files = sorted(workspace.glob("*-task.md"))

    for task_file in task_files:
        content = task_file.read_text()

        # Parse status
        status = None
        if "**Status:** SUCCESS" in content:
            status = "SUCCESS"
        elif "**Status:** PARTIAL" in content:
            status = "PARTIAL"
        elif "**Status:** FAILED" in content:
            status = "FAILED"

        if status is None:
            continue  # Task not completed

        # Extract summary section
        summary_start = content.find("**Summary:**")
        if summary_start == -1:
            continue

        summary_end = content.find("**Artifacts:**", summary_start)
        if summary_end == -1:
            summary_end = summary_start + 500

        summary_text = content[summary_start:summary_end]

        # Infer effort
        effort = infer_effort_from_result(summary_text)

        outcomes.append(
            {
                "task_file": str(task_file.name),
                "status": status,
                "inferred_effort": effort,
                "success": status == "SUCCESS",
            }
        )

    return outcomes
