"""
Memory Consolidation API Routes for CodeContext server.

Handles cluster detection, consolidation decisions, and applying merges.
"""

import json
import time
from typing import Optional

from fastapi import APIRouter, Request, Query, HTTPException
from fastapi.responses import StreamingResponse

from ..models import ConsolidationRunRequest, ConsolidationStreamRequest, ApplyConsolidationRequest
from ..services.store_manager import get_store, list_projects, ALL_PROJECTS
from ..utils.formatting import sse_event, error_response
from ..utils.timestamps import utc_now_iso
from ..memory_consolidator import (
    run_consolidation, scan_clusters, find_clusters, tier_clusters,
    log_consolidation, ConsolidationReport,
    consolidate_with_claude, ConsolidationResult
)
from ..cli_utils import PROJECTS_DIR


router = APIRouter()


def get_config(request: Request):
    """Get server config from app state."""
    return request.app.state.server_config


def get_default_config(request: Request):
    """Get default config from app state."""
    return request.app.state.default_config


@router.post("/api/consolidation/auto-scan")
async def auto_consolidation_scan(
    request: Request,
    project: str = Query(default=""),
    resolve: bool = Query(default=True),
    threshold: float = Query(default=0.5),
    max_clusters: int = Query(default=10)
):
    """
    Scan for clusters and optionally resolve them.

    This is the scan-first approach for session hooks:
    - Always runs the scan (cheap, vector-only)
    - Only calls AI if clusters are found AND resolve=True

    Args:
        project: Project name to scan (uses first available if empty)
        resolve: If True, resolve found clusters using AI
        threshold: Distance threshold for similarity (default 0.55)
        max_clusters: Max clusters to process per call

    Returns scan results even if no clusters found.
    """
    server_config = get_config(request)
    default_config = get_default_config(request)
    start_time = time.time()

    # Check if auto-consolidation is enabled
    consolidation_settings = server_config.get("auto_consolidation", default_config["auto_consolidation"])
    if not consolidation_settings.get("enabled", False):
        return {
            "project": project,
            "scanned": False,
            "clusters_found": 0,
            "message": "Auto-consolidation is disabled",
            "duration_ms": 0
        }

    # Default to first project if not specified
    projects = list_projects()
    if not project:
        if not projects:
            return {
                "project": "",
                "scanned": False,
                "clusters_found": 0,
                "message": "No projects found",
                "duration_ms": 0
            }
        project = projects[0]

    # Skip if project doesn't exist
    if project not in projects:
        return {
            "project": project,
            "scanned": False,
            "clusters_found": 0,
            "message": f"Project '{project}' not found",
            "duration_ms": 0
        }

    try:
        store = get_store(project)

        # Phase 1: Scan for clusters (cheap, vector-only)
        clusters = find_clusters(store, threshold)

        if not clusters:
            return {
                "project": project,
                "scanned": True,
                "clusters_found": 0,
                "auto_resolved": 0,
                "llm_resolved": 0,
                "ai_called": False,
                "message": "No clusters found - memory store is clean",
                "duration_ms": round((time.time() - start_time) * 1000, 2)
            }

        # Phase 2: Resolve if requested
        if not resolve:
            return {
                "project": project,
                "scanned": True,
                "clusters_found": len(clusters),
                "auto_resolved": 0,
                "llm_resolved": 0,
                "ai_called": False,
                "message": f"Found {len(clusters)} clusters (scan only, no resolution)",
                "duration_ms": round((time.time() - start_time) * 1000, 2)
            }

        # Run consolidation (AI will be called for non-auto-tier clusters)
        report = await run_consolidation(
            store=store,
            project=project,
            distance_threshold=threshold,
            max_clusters=max_clusters,
            preview=False
        )

        # Count auto vs LLM resolved
        auto_count = sum(1 for r in report.results if "auto" in r.reasoning.lower())
        llm_count = report.clusters_processed - auto_count

        return {
            "project": project,
            "scanned": True,
            "clusters_found": report.clusters_found,
            "clusters_processed": report.clusters_processed,
            "auto_resolved": auto_count,
            "llm_resolved": llm_count,
            "deleted": report.deleted,
            "ai_called": llm_count > 0,
            "message": f"Consolidated {report.clusters_processed} clusters, deleted {report.deleted} duplicates",
            "duration_ms": round((time.time() - start_time) * 1000, 2)
        }

    except Exception as e:
        import traceback
        print(f"[AutoScan] Error: {e}")
        print(f"[AutoScan] Traceback: {traceback.format_exc()}")
        return error_response(
            detail=str(e),
            code="AUTO_SCAN_FAILED",
            status_code=500
        )


@router.get("/api/consolidation/scan")
async def scan_for_clusters(
    project: str = Query(...),
    threshold: float = Query(default=0.5)
):
    """
    Preview: Scan for clusters that would be consolidated.

    Returns cluster information without making any changes.
    Use this to see what duplicates exist before running consolidation.
    """
    try:
        if project == ALL_PROJECTS:
            # Aggregate clusters from all projects
            projects = list_projects()
            all_clusters = []
            total_by_tier = {"auto": 0, "llm_small": 0, "llm_large": 0}
            total_memories = 0

            for p in projects:
                store = get_store(p)
                clusters = find_clusters(store, threshold)
                tiered = tier_clusters(clusters)

                # Add project to each cluster
                for c in clusters:
                    cluster_dict = c.to_dict()
                    cluster_dict["project"] = p
                    all_clusters.append(cluster_dict)
                    total_memories += c.size

                total_by_tier["auto"] += len(tiered["auto"])
                total_by_tier["llm_small"] += len(tiered["llm_small"])
                total_by_tier["llm_large"] += len(tiered["llm_large"])

            # Sort by size (largest first)
            all_clusters.sort(key=lambda c: -c["size"])

            return {
                "project": project,
                "total_clusters": len(all_clusters),
                "total_memories": total_memories,
                "clusters": all_clusters,
                "tiers": total_by_tier
            }
        else:
            store = get_store(project)
            result = await scan_clusters(store, project, threshold)
            # Add total_memories if not present
            if "total_memories" not in result:
                result["total_memories"] = sum(c["size"] for c in result.get("clusters", []))
            return result
    except Exception as e:
        import traceback
        print(f"[Scan] Error: {e}")
        print(f"[Scan] Traceback: {traceback.format_exc()}")
        raise HTTPException(status_code=500, detail=str(e))


@router.get("/api/consolidation/history")
async def get_consolidation_history(
    project: str = Query(default=""),
    limit: int = Query(default=50),
    offset: int = Query(default=0)
):
    """Get consolidation history from per-project log files."""
    from cli_utils import get_logs_dir

    # Collect log files to read based on project selection
    log_files = []
    if project and project != ALL_PROJECTS:
        # Single project - read its log
        log_file = get_logs_dir(project) / "consolidation.log"
        if log_file.exists():
            log_files.append(log_file)
    else:
        # All projects - aggregate from each project's logs
        for proj_dir in PROJECTS_DIR.iterdir():
            if proj_dir.is_dir():
                log_file = proj_dir / "logs" / "consolidation.log"
                if log_file.exists():
                    log_files.append(log_file)

    if not log_files:
        return {"entries": [], "total": 0, "has_more": False}

    # Read and parse JSON Lines from all relevant log files
    entries = []
    for log_file in log_files:
        with open(log_file, "r", encoding="utf-8") as f:
            for line in f:
                if line.strip():
                    try:
                        entry = json.loads(line)
                        entries.append(entry)
                    except json.JSONDecodeError:
                        continue  # Skip malformed lines

    # Sort by timestamp descending (newest first)
    entries.sort(key=lambda x: x.get("timestamp", ""), reverse=True)

    # Pagination
    total = len(entries)
    paginated = entries[offset:offset + limit]

    return {
        "entries": paginated,
        "total": total,
        "has_more": offset + limit < total
    }


@router.get("/api/consolidation/stream")
async def consolidation_stream(
    project: str = Query(...),
    threshold: float = Query(default=0.5),
    max_clusters: int = Query(default=10)
):
    """
    Stream real-time progress updates during consolidation via Server-Sent Events.

    Progress phases:
    1. starting - Initial setup
    2. finding_clusters - Scanning for similar memories
    3. found_clusters - Clusters identified with count
    4. tiering - Organizing clusters by complexity
    5. processing_auto - Auto-resolving simple clusters
    6. processing_llm - Sending clusters to Claude for analysis
    7. processing_cluster - Per-cluster progress during LLM processing
    8. complete - Finished with results

    Returns: StreamingResponse with SSE events
    """
    if project == ALL_PROJECTS:
        async def error_stream():
            yield sse_event({
                "phase": "error",
                "message": "Cannot run consolidation on 'All Projects'. Please select a specific project.",
                "progress": 0
            })
        return StreamingResponse(error_stream(), media_type="text/event-stream")

    store = get_store(project)

    async def generate():
        timestamp = utc_now_iso()

        try:
            # Phase 1: Starting
            yield sse_event({
                "phase": "starting",
                "message": "Initializing consolidation...",
                "progress": 0
            })

            # Phase 2: Finding clusters
            yield sse_event({
                "phase": "finding_clusters",
                "message": "Scanning for similar memory clusters...",
                "progress": 5
            })

            clusters = find_clusters(store, threshold)

            if not clusters:
                yield sse_event({
                    "phase": "complete",
                    "message": "No clusters found",
                    "progress": 100,
                    "results": {
                        "clusters_found": 0,
                        "clusters_processed": 0,
                        "consolidated": 0,
                        "kept": 0,
                        "deleted": 0,
                        "results": []
                    }
                })
                return

            # Phase 3: Found clusters
            yield sse_event({
                "phase": "found_clusters",
                "message": f"Found {len(clusters)} clusters",
                "count": len(clusters),
                "progress": 15
            })

            # Phase 4: Tiering
            yield sse_event({
                "phase": "tiering",
                "message": "Organizing clusters by complexity...",
                "progress": 20
            })

            tiered = tier_clusters(clusters)

            yield sse_event({
                "phase": "tiered",
                "message": f"LLM Small: {len(tiered['llm_small'])}, LLM Large: {len(tiered['llm_large'])}",
                "tiers": {
                    "auto": len(tiered["auto"]),  # Always 0 - kept for API compatibility
                    "llm_small": len(tiered["llm_small"]),
                    "llm_large": len(tiered["llm_large"])
                },
                "progress": 25
            })

            # All clusters go to LLM - no auto-processing
            results = []

            # Phase 6: Process LLM clusters
            remaining_budget = max_clusters - len(results)

            if remaining_budget > 0:
                llm_clusters = (tiered["llm_small"] + tiered["llm_large"])[:remaining_budget]

                if llm_clusters:
                    yield sse_event({
                        "phase": "processing_llm",
                        "message": f"Sending {len(llm_clusters)} clusters to Claude for analysis...",
                        "count": len(llm_clusters),
                        "progress": 45
                    })

                    # Call Claude for consolidation decisions
                    decisions = await consolidate_with_claude(llm_clusters)

                    yield sse_event({
                        "phase": "llm_complete",
                        "message": f"Claude analyzed {len(decisions)} clusters",
                        "count": len(decisions),
                        "progress": 85
                    })

                    # Map decisions to clusters
                    decision_by_cluster = {d["cluster_id"]: d for d in decisions}

                    # Process each cluster decision
                    for i, cluster in enumerate(llm_clusters):
                        decision = decision_by_cluster.get(cluster.id)

                        if decision:
                            # Preview mode - calculate what WOULD happen
                            decision_type = decision.get("decision", "unknown")
                            kept_id = decision.get("keep_id")

                            if decision_type == "keep_one" and kept_id:
                                deleted_ids = [mid for mid in cluster.memory_ids if mid != kept_id]
                            elif decision_type == "merge":
                                deleted_ids = list(cluster.memory_ids)
                            else:
                                deleted_ids = []

                            all_files = []
                            if decision_type == "merge":
                                for mem in cluster.memories:
                                    all_files.extend(mem.get("files", []))
                                all_files = list(set(all_files))

                            results.append(ConsolidationResult(
                                cluster_id=cluster.id,
                                decision=decision_type,
                                kept_id=kept_id,
                                merged_memory={
                                    "type": decision.get("merged_type"),
                                    "title": decision.get("merged_title"),
                                    "fact": decision.get("merged_fact"),
                                    "files": all_files
                                } if decision_type == "merge" else None,
                                deleted_ids=deleted_ids,
                                reasoning=decision.get("reasoning", "")
                            ))
                        else:
                            results.append(ConsolidationResult(
                                cluster_id=cluster.id,
                                decision="error",
                                reasoning="No decision returned by LLM"
                            ))

                        # Progress update per cluster
                        cluster_progress = 85 + (i + 1) / len(llm_clusters) * 10
                        yield sse_event({
                            "phase": "processing_cluster",
                            "message": f"Processed cluster {i+1}/{len(llm_clusters)}",
                            "current": i + 1,
                            "total": len(llm_clusters),
                            "cluster_id": cluster.id,
                            "decision": decision.get("decision") if decision else "error",
                            "progress": int(cluster_progress)
                        })

            # Calculate final stats
            consolidated = sum(1 for r in results if r.decision == "merge")
            kept = sum(1 for r in results if r.decision in ("keep_one", "keep_all"))
            deleted = sum(len(r.deleted_ids) for r in results)

            # Phase 7: Complete
            yield sse_event({
                "phase": "complete",
                "message": f"Processed {len(results)} clusters: {consolidated} to merge, {kept} to keep",
                "progress": 100,
                "results": {
                    "project": project,
                    "preview": True,
                    "timestamp": timestamp,
                    "clusters_found": len(clusters),
                    "clusters_processed": len(results),
                    "consolidated": consolidated,
                    "kept": kept,
                    "deleted": deleted,
                    "results": [r.to_dict() for r in results]
                }
            })

        except Exception as e:
            import traceback
            print(f"[Consolidation Stream] Error: {e}")
            print(f"[Consolidation Stream] Traceback: {traceback.format_exc()}")
            yield sse_event({
                "phase": "error",
                "message": f"Consolidation failed: {str(e)}",
                "progress": 0
            })

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no"  # Disable nginx buffering if present
        }
    )


@router.post("/api/consolidation/run")
async def run_consolidation_endpoint(
    project: str = Query(...),
    preview: bool = Query(default=True),
    req: Optional[ConsolidationRunRequest] = None
):
    """
    Run group consolidation - find and merge duplicate clusters.

    This process:
    1. Scans for clusters of semantically similar memories
    2. Tiers clusters by complexity (auto vs LLM)
    3. Uses Claude to decide: keep_best, merge, or discard
    4. Applies decisions (unless preview=True)

    Args:
        project: The project name (required, cannot use __all__ for run)
        preview: If True, return proposed decisions without applying them
        req body: Optional threshold and max_clusters settings
    """
    if project == ALL_PROJECTS:
        raise HTTPException(
            status_code=400,
            detail="Cannot run consolidation on 'All Projects'. Please select a specific project."
        )

    # Get parameters from request body or use defaults
    threshold = req.threshold if req else 0.5
    max_clusters = req.max_clusters if req else 10

    store = get_store(project)

    try:
        report = await run_consolidation(
            store=store,
            project=project,
            distance_threshold=threshold,
            max_clusters=max_clusters,
            preview=preview
        )

        return {
            "project": project,
            "preview": preview,
            "timestamp": report.timestamp,
            "clusters_found": report.clusters_found,
            "clusters_processed": report.clusters_processed,
            "consolidated": report.consolidated,
            "kept": report.kept,
            "deleted": report.deleted,
            "results": [r.to_dict() for r in report.results],
            "errors": report.errors if report.errors else None
        }

    except Exception as e:
        import traceback
        print(f"[Consolidation] Error: {e}")
        print(f"[Consolidation] Traceback: {traceback.format_exc()}")
        raise HTTPException(status_code=500, detail=f"Consolidation failed: {str(e)}")


@router.post("/api/consolidation/apply")
async def apply_consolidation_decisions(
    project: str = Query(...),
    req: ApplyConsolidationRequest = None
):
    """
    Apply user-approved consolidation decisions.

    This endpoint applies decisions from a preview run that the user has approved.
    """
    if project == ALL_PROJECTS:
        raise HTTPException(
            status_code=400,
            detail="Cannot apply consolidation on 'All Projects'. Please select a specific project."
        )

    if not req or not req.results:
        raise HTTPException(status_code=400, detail="No results to apply")

    store = get_store(project)
    applied = []
    errors = []

    for result in req.results:
        decision = result.get("decision")
        cluster_id = result.get("cluster_id")

        try:
            if decision == "keep_one":
                # Delete all except kept_id
                kept_id = result.get("kept_id")
                deleted_ids = result.get("deleted_ids", [])
                if deleted_ids:
                    store.delete_memories(deleted_ids)
                applied.append({"cluster_id": cluster_id, "action": "keep_one", "kept": kept_id})

            elif decision == "merge":
                # Get merged memory details
                merged = result.get("merged_memory", {})
                deleted_ids = result.get("deleted_ids", [])

                if merged and merged.get("title") and merged.get("fact"):
                    # Add consolidated memory
                    new_mem = store.add_consolidated_memory(
                        obs_type=merged.get("type", "discovery"),
                        title=merged["title"],
                        fact=merged["fact"],
                        files=merged.get("files", []),
                        source_ids=deleted_ids
                    )
                    # Delete originals
                    if deleted_ids:
                        store.delete_memories(deleted_ids)
                    applied.append({
                        "cluster_id": cluster_id,
                        "action": "merge",
                        "new_id": new_mem["id"]
                    })
                else:
                    errors.append({
                        "cluster_id": cluster_id,
                        "error": "Missing merged memory details"
                    })

            elif decision == "keep_all":
                # Nothing to do
                applied.append({"cluster_id": cluster_id, "action": "keep_all"})

            else:
                errors.append({
                    "cluster_id": cluster_id,
                    "error": f"Unknown decision: {decision}"
                })

        except Exception as e:
            errors.append({
                "cluster_id": cluster_id,
                "error": str(e)
            })

    # Log the consolidation for history tracking
    if applied:
        report = ConsolidationReport(
            project=project,
            timestamp=utc_now_iso(),
            clusters_found=len(req.results),
            clusters_processed=len(applied),
            consolidated=sum(1 for r in applied if r.get("action") == "merge"),
            kept=sum(1 for r in applied if r.get("action") in ("keep_one", "keep_all")),
            deleted=sum(len(r.get("deleted_ids", [])) for r in req.results if r.get("decision") != "keep_all"),
            results=[],  # Results already applied, just logging summary
            errors=errors
        )
        log_consolidation(project, report)

    return {
        "project": project,
        "applied": len(applied),
        "results": applied,
        "errors": errors if errors else None
    }
