"""Report module - generates .cloven.md session reports for accumulated evidence.

These reports capture:
1. Initial assessment predictions
2. Actual execution outcomes
3. Accuracy analysis
4. Calibration insights

Reports accumulate over time as artifacts that can inform future development cycles.
"""

from __future__ import annotations

import logging
from datetime import datetime
from pathlib import Path

from cleave.core.file_utils import atomic_write_text
from cleave.core.metrics import (
    EFFORT_TO_COMPLEXITY,
    calculate_accuracy,
    get_metrics_summary,
)
from cleave.core.settings import generate_cloven_filename, load_settings
from cleave.core.yaml_utils import parse_yaml_simple

logger = logging.getLogger(__name__)


def _get_version() -> str:
    """Get cleave version, avoiding circular import."""
    try:
        # Import at function level to avoid circular dependency
        from cleave import __version__
        return __version__
    except ImportError:
        return "unknown"


def generate_cloven_report(
    workspace_path: str | Path,
    aggregated: dict,
    conflicts: list,
    extracted_effort: list[dict],
    explicit_effort: str | None = None,
) -> str:
    """Generate a .cloven.md report for a completed cleave session.

    Enhanced to include comprehensive metrics from all workspace artifacts:
    - manifest.yaml (intent, assessment, ancestry, children)
    - siblings.yaml (coordination, status, exports)
    - metrics.yaml (telemetry)
    - task results (full summaries, verification, decisions)
    - merge.md/review.md (if present)

    Args:
        workspace_path: Path to the workspace
        aggregated: Aggregated results from reunify
        conflicts: List of detected conflicts
        extracted_effort: List of effort extractions from tasks
        explicit_effort: Optional explicit effort override

    Returns:
        Markdown content for the report
    """
    # Validate aggregated dict structure
    required_keys = [
        "rollup_status", "task_count", "by_task", "verification_summary",
        "all_decisions", "all_interfaces", "all_assumptions"
    ]
    missing = [k for k in required_keys if k not in aggregated]
    if missing:
        raise ValueError(f"aggregated dict missing required keys: {missing}")

    workspace = Path(workspace_path)
    timestamp = datetime.now().isoformat()

    # Load manifest for assessment data
    manifest_path = workspace / "manifest.yaml"
    manifest = {}
    assessment = {}
    ancestry = {}
    children_info = []
    intent = {}
    if manifest_path.exists():
        try:
            manifest = parse_yaml_simple(manifest_path.read_text())
            assessment = manifest.get("assessment", {})
            ancestry = manifest.get("ancestry", {})
            children_info = manifest.get("children", [])
            intent = manifest.get("intent", {})
        except Exception as e:
            logger.warning(f"Failed to parse manifest.yaml: {e}")
            manifest = {}

    # Load siblings data
    siblings_path = workspace / "siblings.yaml"
    siblings_data = {}
    coordination = {}
    if siblings_path.exists():
        try:
            siblings_data = parse_yaml_simple(siblings_path.read_text())
            coordination = siblings_data.get("coordination", {})
        except Exception as e:
            logger.warning(f"Failed to parse siblings.yaml: {e}")
            siblings_data = {}

    # Load metrics/telemetry
    metrics_path = workspace / "metrics.yaml"
    telemetry = {}
    if metrics_path.exists():
        try:
            metrics_data = parse_yaml_simple(metrics_path.read_text())
            telemetry = metrics_data.get("telemetry", {})
        except Exception as e:
            logger.warning(f"Failed to parse metrics.yaml: {e}")
            telemetry = {}

    # Load merge.md and review.md if they exist
    merge_path = workspace / "merge.md"
    merge_content = None
    if merge_path.exists():
        merge_content = merge_path.read_text()

    review_path = workspace / "review.md"
    review_content = None
    if review_path.exists():
        review_content = review_path.read_text()

    # Extract prediction data
    root_directive = manifest.get("root_directive", "[unknown]")
    predicted_complexity = assessment.get("complexity", 0.0)
    predicted_decision = assessment.get("decision", "unknown")
    pattern_matched = assessment.get("pattern", None)
    pattern_confidence = assessment.get("confidence", 0.0)
    # Note: systems is an int (count), modifiers is a list
    systems_count = assessment.get("systems", 0)
    modifiers_applied = assessment.get("modifiers", [])

    # Determine actual effort
    if explicit_effort:
        actual_effort = explicit_effort
        effort_source = "explicit override"
    elif extracted_effort:
        efforts = [e["inferred_effort"] for e in extracted_effort if e.get("inferred_effort")]
        if efforts:
            actual_effort = max(set(efforts), key=efforts.count)
            effort_source = "inferred from task results"
        else:
            actual_effort = "unknown"
            effort_source = "could not infer"
    else:
        actual_effort = "unknown"
        effort_source = "no data"

    # Calculate accuracy if we have both prediction and outcome
    accuracy_score = None
    accuracy_analysis = ""
    if actual_effort in EFFORT_TO_COMPLEXITY and predicted_complexity > 0:
        accuracy_score = calculate_accuracy(predicted_complexity, actual_effort)
        min_exp, max_exp = EFFORT_TO_COMPLEXITY[actual_effort]

        if accuracy_score >= 0.9:
            accuracy_analysis = "Prediction was accurate."
        elif accuracy_score >= 0.6:
            accuracy_analysis = f"Prediction was close. Expected range for '{actual_effort}' effort: {min_exp}-{max_exp}."
        else:
            if predicted_complexity < min_exp:
                accuracy_analysis = f"Under-predicted complexity. Predicted {predicted_complexity}, but '{actual_effort}' effort suggests {min_exp}-{max_exp}."
            else:
                accuracy_analysis = f"Over-predicted complexity. Predicted {predicted_complexity}, but '{actual_effort}' effort suggests {min_exp}-{max_exp}."

    # Build task outcomes table
    task_rows = []
    for task in aggregated.get("by_task", []):
        task_id = task.get("id", "?")
        status = task.get("status", "PENDING")
        files = task.get("file_count") or 0  # Handle None values
        verified = "✓" if task.get("verified") else "✗"

        # Find matching effort extraction
        effort_match = next(
            (e for e in extracted_effort if str(task_id) in e.get("task_file", "")),
            None,
        )
        inferred = effort_match.get("inferred_effort", "-") if effort_match else "-"

        task_rows.append(f"| {task_id} | {status} | {files} | {verified} | {inferred} |")

    task_table = "\n".join(task_rows) if task_rows else "| - | - | - | - | - |"

    # Build conflict summary
    conflict_count = len(conflicts)
    if conflict_count == 0:
        conflict_summary = "No conflicts detected."
    else:
        conflict_types = {}
        for c in conflicts:
            ctype = c.type if hasattr(c, "type") else c.get("type", "unknown")
            conflict_types[ctype] = conflict_types.get(ctype, 0) + 1
        conflict_summary = ", ".join(f"{k}: {v}" for k, v in conflict_types.items())

    # Build calibration insight
    calibration_insight = _generate_calibration_insight(
        predicted_complexity=predicted_complexity,
        actual_effort=actual_effort,
        pattern_matched=pattern_matched,
        accuracy_score=accuracy_score,
        systems_count=systems_count,
        modifiers_applied=modifiers_applied,
    )

    # Format systems count and modifiers (handle None values)
    systems_str = str(systems_count or 0)
    modifiers_str = ", ".join(modifiers_applied) if modifiers_applied else "none applied"

    # Build ancestry context
    depth = ancestry.get("depth") or 0
    node_id = ancestry.get("node_id", "root")
    remaining_budget = ancestry.get("remaining_budget") or 0
    parent_chain = ancestry.get("parent_chain", [])

    ancestry_str = f"Depth {depth}, Node {node_id}, Budget {remaining_budget}"
    if parent_chain:
        chain_str = " → ".join([f"{p.get('node_id', '?')}" for p in parent_chain])
        ancestry_str += f"\n**Chain:** {chain_str} → {node_id}"

    # Build children summary (handle malformed children_info)
    children_str = ", ".join([
        f"{c.get('id', '?')}: {c.get('label', '[unlabeled]')}"
        for c in children_info
        if isinstance(c, dict)
    ])

    # Build intent section
    goal_str = intent.get("goal", "[not specified]")
    success_criteria = intent.get("success_criteria", [])
    constraints = intent.get("constraints", [])
    out_of_scope = intent.get("out_of_scope", [])

    criteria_str = "\n".join([f"- {c}" for c in success_criteria]) if success_criteria else "- [none specified]"
    constraints_str = "\n".join([f"- {c}" for c in constraints]) if constraints else "- [none specified]"
    out_of_scope_str = "\n".join([f"- {s}" for s in out_of_scope]) if out_of_scope else "- [none specified]"

    # Build coordination summary
    coord_mode = coordination.get("mode", "unknown")
    coord_depth = coordination.get("depth") or 0
    coord_parent = coordination.get("parent_node", "unknown")

    # Build telemetry summary
    telemetry_str = ""
    if telemetry:
        telemetry_str = f"""
**Ancestry Access:** {telemetry.get('ancestry_access_count') or 0}
**Sibling Access:** {telemetry.get('sibling_access_count') or 0}
**Intent References:** {telemetry.get('intent_reference_count') or 0}
**Cache Hits:** {telemetry.get('cache_hits') or 0}
**Cache Misses:** {telemetry.get('cache_misses') or 0}
**Fast Path Hits:** {telemetry.get('fast_path_hits') or 0}
**Sequential Fallback:** {telemetry.get('sequential_fallback') or 0}"""
    else:
        telemetry_str = "*No telemetry data available*"

    # Build verification details
    verification_details = []
    for v in aggregated.get("verification_summary", []):
        task_id = v.get("task_id")
        has_verification = v.get("has_verification", False)
        command = v.get("command", "[not provided]")
        output = v.get("output", "[no output]")
        coverage = v.get("coverage", "[not measured]")
        edge_cases = v.get("edge_cases", [])

        edge_str = ", ".join(edge_cases) if edge_cases else "none listed"

        # Format output with safe truncation
        output_str = str(output)
        if len(output_str) > 200:
            output_display = f"{output_str[:200]}..."
        else:
            output_display = output_str

        verification_details.append(f"""
#### Task {task_id}
**Verified:** {"✓" if has_verification else "✗"}
**Command:** `{command}`
**Output:** {output_display}
**Coverage:** {coverage}
**Edge Cases:** {edge_str}""")

    verification_section = "\n".join(verification_details) if verification_details else "*No verification data*"

    # Build decisions, interfaces, assumptions
    decisions_str = "\n".join([f"- {d}" for d in aggregated.get("all_decisions", [])]) if aggregated.get("all_decisions") else "- [none recorded]"
    interfaces_str = "\n".join([f"- `{i}`" for i in aggregated.get("all_interfaces", [])]) if aggregated.get("all_interfaces") else "- [none recorded]"
    assumptions_str = "\n".join([f"- {a}" for a in aggregated.get("all_assumptions", [])]) if aggregated.get("all_assumptions") else "- [none recorded]"

    # Build task detail section (full summaries)
    task_details = []
    for task in aggregated.get("by_task", []):
        task_id = task.get("id")
        status = task.get("status", "UNKNOWN")
        summary = task.get("summary", "[no summary]")
        file_count = task.get("file_count") or 0  # Handle None
        interface_count = task.get("interface_count") or 0  # Handle None
        verified = "✓" if task.get("verified") else "✗"
        alignment = task.get("alignment", {})

        alignment_str = ""
        if alignment:
            alignment_str = f"\n**Alignment:** {alignment.get('assessment', '[not provided]')}"

        task_details.append(f"""
#### Task {task_id} ({status})
**Summary:** {summary}
**Files Modified:** {file_count}
**Interfaces Published:** {interface_count}
**Verified:** {verified}{alignment_str}""")

    task_detail_section = "\n".join(task_details) if task_details else "*No task details*"

    # Build merge/review summaries
    merge_summary = ""
    if merge_content:
        # Extract first 500 chars of merge.md
        merge_summary = f"""
### Merge Resolution

{merge_content[:500]}{"..." if len(merge_content) > 500 else ""}

*Full content in `merge.md`*"""

    review_summary = ""
    if review_content:
        # Extract first 500 chars of review.md
        review_summary = f"""
### Adversarial Review

{review_content[:500]}{"..." if len(review_content) > 500 else ""}

*Full content in `review.md`*"""

    # Format reasoning with safe truncation
    reasoning = assessment.get("reasoning", "[not provided]")
    reasoning_str = str(reasoning) if reasoning else "[not provided]"
    if len(reasoning_str) > 200:
        reasoning_display = f"{reasoning_str[:200]}..."
    else:
        reasoning_display = reasoning_str

    # Build next steps list for GitHub template
    next_steps = ["- [ ] Review task outputs"]
    if conflict_count > 0:
        next_steps.append("- [ ] Resolve conflicts (see merge.md)")
    if review_content:
        next_steps.append("- [ ] Address review findings (see review.md)")
    next_steps.extend([
        "- [ ] Validate integration",
        "- [ ] Update documentation"
    ])
    next_steps_str = "\n".join(next_steps)

    report = f"""# Cleave Session Report

**Generated:** {timestamp}
**Workspace:** `{workspace_path}`
**Directive:** {root_directive[:100]}{"..." if len(root_directive) > 100 else ""}

---

## Executive Summary

**Rollup Status:** {aggregated.get("rollup_status", "UNKNOWN")}
**Tasks:** {aggregated.get("task_count", 0)}
**Conflicts:** {len(conflicts)}
**Predicted Complexity:** {predicted_complexity:.1f}
**Actual Effort:** {actual_effort if actual_effort != "unknown" else "[pending]"}
**Accuracy:** {f"{accuracy_score:.0%}" if accuracy_score is not None else "N/A"}

---

## Directive & Intent

### Root Goal
{goal_str}

### Success Criteria
{criteria_str}

### Constraints
{constraints_str}

### Out of Scope
{out_of_scope_str}

---

## Ancestry Context

{ancestry_str}

**Children:** {children_str if children_str else "[none]"}
**Coordination Mode:** {coord_mode}

---

## Assessment Prediction

| Metric | Value |
|--------|-------|
| Pattern Matched | {pattern_matched or "none"} |
| Pattern Confidence | {pattern_confidence:.0%} |
| Predicted Complexity | {predicted_complexity:.1f} |
| Decision | {predicted_decision} |
| Systems Count | {systems_str} |
| Modifiers Applied | {modifiers_str} |
| Method | {assessment.get("method", "unknown")} |
| Reasoning | {reasoning_display} |

---

## Execution Outcome

| Metric | Value |
|--------|-------|
| Rollup Status | {aggregated.get("rollup_status", "UNKNOWN")} |
| Tasks Completed | {aggregated.get("task_count", 0)} |
| Conflicts Found | {conflict_count} |
| Conflict Types | {conflict_summary} |

### Task Breakdown

| Task | Status | Files | Verified | Effort |
|------|--------|-------|----------|--------|
{task_table}

---

## Task Details

{task_detail_section}

---

## Verification Evidence

{verification_section}

---

## Decisions & Interfaces

### Decisions Made
{decisions_str}

### Interfaces Published
{interfaces_str}

### Assumptions
{assumptions_str}

---

## Conflicts

{f"**Total Conflicts:** {conflict_count}" if conflict_count > 0 else "**No conflicts detected.**"}

{conflict_summary if conflict_count > 0 else ""}

{merge_summary if merge_summary else ""}

{review_summary if review_summary else ""}

---

## Telemetry

{telemetry_str}

---

## Accuracy Analysis

| Metric | Value |
|--------|-------|
| Actual Effort | {actual_effort} ({effort_source}) |
| Accuracy Score | {f"{accuracy_score:.0%}" if accuracy_score is not None else "N/A"} |

**Analysis:** {accuracy_analysis if accuracy_analysis else "Insufficient data for analysis."}

---

## Calibration Insight

{calibration_insight}

---

## GitHub Issue Template

Ready-to-paste issue content:

```markdown
### Summary

Cleave session completed: {root_directive[:80]}{"..." if len(root_directive) > 80 else ""}

**Status:** {aggregated.get("rollup_status", "UNKNOWN")}
**Tasks:** {aggregated.get("task_count", 0)}
**Conflicts:** {conflict_count}
**Accuracy:** {f"{accuracy_score:.0%}" if accuracy_score is not None else "N/A"}

### Assessment

- **Pattern:** {pattern_matched or "none"}
- **Predicted Complexity:** {predicted_complexity:.1f}
- **Actual Effort:** {actual_effort}
- **Systems:** {systems_str}
- **Modifiers:** {modifiers_str}

### Outcome

- **Rollup Status:** {aggregated.get("rollup_status", "UNKNOWN")}
- **Conflicts:** {conflict_count}
- **Tasks Completed:** {aggregated.get("task_count", 0)}

### Key Decisions

{decisions_str}

### Interfaces

{interfaces_str}

### Next Steps

{next_steps_str}

### Workspace

`{workspace_path}`

### Report

See `.cloven.md` for full details.
```

---

## Evidence for Future Sessions

This report can be provided as context for future cleave assessments to improve calibration:

```
Previous cleave on similar directive:
- Pattern: {pattern_matched or "none"}
- Predicted complexity: {predicted_complexity:.1f}
- Actual effort: {actual_effort}
- Accuracy: {f"{accuracy_score:.0%}" if accuracy_score is not None else "unknown"}
```

---

*Report generated by cleave v{_get_version()}*
"""

    return report


def _generate_calibration_insight(
    predicted_complexity: float,
    actual_effort: str,
    pattern_matched: str | None,
    accuracy_score: float | None,
    systems_count: int,
    modifiers_applied: list,
) -> str:
    """Generate actionable calibration insight based on prediction vs outcome."""
    insights = []

    if accuracy_score is None:
        return "Insufficient data to generate calibration insights. Ensure tasks are completed with result summaries."

    if accuracy_score >= 0.9:
        insights.append("**Assessment was well-calibrated.** No adjustment needed for this pattern.")
    elif accuracy_score >= 0.6:
        insights.append("**Assessment was reasonably accurate.** Minor refinements may improve precision.")
    else:
        # Significant miss - provide specific guidance
        if actual_effort == "high" and predicted_complexity < 4.0:
            insights.append(
                f"**Under-prediction detected.** Predicted {predicted_complexity:.1f} but actual effort was high."
            )
            if pattern_matched:
                insights.append(
                    f"Consider increasing `systems_base` for pattern '{pattern_matched}' by 1.0-1.5."
                )
            if systems_count < 3:
                insights.append(
                    "Few systems were detected. The complexity formula may need additional system indicators."
                )
        elif actual_effort == "low" and predicted_complexity > 2.5:
            insights.append(
                f"**Over-prediction detected.** Predicted {predicted_complexity:.1f} but actual effort was low."
            )
            if pattern_matched:
                insights.append(
                    f"Consider decreasing `systems_base` for pattern '{pattern_matched}' by 0.5-1.0."
                )
            if modifiers_applied:
                insights.append(
                    f"Modifiers applied ({', '.join(modifiers_applied)}) may be over-weighting complexity."
                )

    if not insights:
        insights.append("Review the assessment parameters if this pattern recurs with similar accuracy.")

    return "\n\n".join(insights)


def write_cloven_report(
    workspace_path: str | Path,
    aggregated: dict,
    conflicts: list,
    extracted_effort: list[dict],
    explicit_effort: str | None = None,
) -> Path:
    """Generate and write .cloven.md report based on settings.

    The report filename and location are controlled by ~/.cleave/settings.yaml:
    - cloven_report_path: Directory for reports (default: workspace)
    - cloven_report_naming: "descriptor", "timestamp", or "workspace"

    Returns:
        Path to the written report file
    """
    workspace = Path(workspace_path)

    # Load manifest to get directive for filename generation
    manifest_path = workspace / "manifest.yaml"
    directive = "[unknown directive]"
    if manifest_path.exists():
        try:
            manifest = parse_yaml_simple(manifest_path.read_text())
            directive = manifest.get("root_directive", directive)
        except Exception:
            pass

    report_content = generate_cloven_report(
        workspace_path=workspace_path,
        aggregated=aggregated,
        conflicts=conflicts,
        extracted_effort=extracted_effort,
        explicit_effort=explicit_effort,
    )

    # Generate filename based on settings
    settings = load_settings()
    report_path = generate_cloven_filename(
        directive=directive,
        workspace_path=workspace,
        settings=settings,
    )

    atomic_write_text(report_path, report_content)
    return report_path


def get_accumulated_reports(project_root: str | Path = ".") -> list[dict]:
    """Find all .cloven.md reports in a project for accumulated evidence.

    Args:
        project_root: Root directory to search

    Returns:
        List of report summaries with paths and key metrics
    """
    root = Path(project_root).resolve()
    reports = []

    # Search for .cloven.md files recursively
    for report_path in root.rglob(".cloven.md"):
        content = report_path.read_text()

        # Extract key metrics from report
        summary = {
            "path": str(report_path),
            "workspace": str(report_path.parent),
        }

        # Parse directive (rough extraction)
        if "**Directive:**" in content:
            start = content.find("**Directive:**") + len("**Directive:**")
            end = content.find("\n", start)
            summary["directive"] = content[start:end].strip()[:80]

        # Parse pattern
        if "| Pattern Matched |" in content:
            start = content.find("| Pattern Matched |") + len("| Pattern Matched |")
            end = content.find("|", start)
            summary["pattern"] = content[start:end].strip()

        # Parse accuracy
        if "| Accuracy Score |" in content:
            start = content.find("| Accuracy Score |") + len("| Accuracy Score |")
            end = content.find("|", start)
            accuracy_str = content[start:end].strip()
            if accuracy_str != "N/A":
                try:
                    summary["accuracy"] = float(accuracy_str.replace("%", "")) / 100
                except ValueError:
                    pass

        # Parse actual effort
        if "| Actual Effort |" in content:
            start = content.find("| Actual Effort |") + len("| Actual Effort |")
            end = content.find("(", start)
            if end == -1:
                end = content.find("|", start)
            summary["actual_effort"] = content[start:end].strip()

        reports.append(summary)

    return reports
