"""Memory API — generate, read, search, and health-check memory files."""

from __future__ import annotations

import logging

from fastapi import APIRouter, HTTPException, Request

log = logging.getLogger(__name__)

router = APIRouter(prefix="/memory", tags=["memory"])


@router.post("/generate")
async def generate_daily_memory(request: Request, date: str | None = None) -> dict:
    """Generate daily memory file for a given date (defaults to today)."""
    engine = request.app.state.memory_engine
    return await engine.generate_daily_memory(date)


@router.get("/today")
async def get_today_memory(request: Request) -> dict:
    """Get today's memory file."""
    from agntspace.infra.timezone import local_today

    engine = request.app.state.memory_engine
    date = local_today()
    content = engine.read_daily_memory(date)
    return {"date": date, "content": content, "exists": content is not None}


@router.get("/dates")
async def list_memory_dates(request: Request, limit: int = 30) -> list[str]:
    """List dates that have memory files."""
    engine = request.app.state.memory_engine
    return engine.list_memory_dates(limit=limit)


@router.post("/compact")
async def compact_memory(request: Request, layer: str | None = None) -> dict:
    """Run layered memory compaction.

    If `layer` is specified (week/month/quarter/year/longterm), compact only that layer.
    If omitted, compacts all layers bottom-up and assembles MEMORY.md.
    """
    engine = request.app.state.memory_engine
    return await engine.compact_memory(layer=layer)


@router.get("/permanent/read")
async def get_permanent_memory(request: Request) -> dict:
    """Get MEMORY.md — assembled view from all summary layers."""
    vault = request.app.state.vault
    if vault.exists("memory/MEMORY.md"):
        doc = vault.read("memory/MEMORY.md")
        return {"content": doc.content, "exists": True}
    return {"content": None, "exists": False}


@router.get("/summaries")
async def list_summaries(request: Request) -> list[dict]:
    """List all memory summary layers with content and metadata."""
    engine = request.app.state.memory_engine
    return engine.list_summaries()


@router.get("/summaries/{layer}")
async def get_summary(request: Request, layer: str) -> dict:
    """Get a specific memory summary layer (week/month/quarter/year/longterm)."""
    valid = ("week", "month", "quarter", "year", "longterm")
    if layer not in valid:
        raise HTTPException(status_code=400, detail=f"Invalid layer. Must be one of: {', '.join(valid)}")
    engine = request.app.state.memory_engine
    return engine.read_summary(layer)


@router.get("/pending")
async def list_pending_memory(request: Request, include_approved: bool = False) -> list[dict]:
    """List pending memory updates awaiting user approval.

    With ``include_approved=true``, also returns approved items so the
    UI can offer a "revert to pending" action.
    """
    engine = request.app.state.memory_engine
    items = engine.list_pending_memory()
    if include_approved:
        approved = engine.list_pending_memory(status="approved")
        for a in approved:
            a["approved"] = True
        items.extend(approved)
    return items


@router.get("/pending/history")
async def list_memory_review_history(request: Request) -> list[dict]:
    """Return all memory review items with their status (pending, approved, rejected)."""
    engine = request.app.state.memory_engine
    result = []
    for status in ("pending", "approved", "rejected"):
        items = engine.list_pending_memory(status=status)
        for item in items:
            item["status"] = status
        result.extend(items)
    # Sort by date descending
    result.sort(key=lambda x: x.get("date", ""), reverse=True)
    return result


@router.get("/review-history")
async def get_review_history(
    request: Request,
    surface: str | None = None,
    action: str | None = None,
    since: str | None = None,
    limit: int = 50,
) -> dict:
    """Query review history across all surfaces.

    Query params:
        surface: filter by surface (memory/reflection/dream/relationship)
        action: filter by action (approved/rejected/unapproved/restored/batch_approved/batch_confirmed)
        since: ISO timestamp lower bound
        limit: max results (default 50)
    """
    review_history = getattr(request.app.state, "review_history", None)
    if not review_history:
        return {"history": [], "count": 0}
    items = await review_history.query(surface=surface, action=action, since=since, limit=limit)
    return {"history": items, "count": len(items)}


@router.get("/review-history/stats")
async def get_review_history_stats(request: Request) -> dict:
    """Return aggregate review history stats by surface and action."""
    review_history = getattr(request.app.state, "review_history", None)
    if not review_history:
        return {"by_surface": {}, "total": 0}
    return await review_history.stats()


@router.get("/pending/{date}/content")
async def get_pending_content(request: Request, date: str) -> dict:
    """Return the raw content of a pending memory update for editing."""
    reader = request.app.state.vault
    pending_path = f"memory/pending/memory-updates_{date}.md"
    if not reader.exists(pending_path):
        raise HTTPException(status_code=404, detail=f"No pending updates for {date}")
    doc = reader.read(pending_path)
    return {"date": date, "content": doc.content or ""}


@router.post("/pending/{date}/approve")
async def approve_memory_updates(request: Request, date: str) -> dict:
    """Approve pending memory updates for a date → adds to MEMORY.md.

    If a JSON body with ``content`` is provided, the pending file is
    rewritten with the edited content before the normal approval flow
    runs.  This lets users edit-then-approve in one step.
    """
    # If the user sent edited content, rewrite the pending file first
    try:
        body = await request.json()
    except Exception:
        body = {}
    edited = body.get("content") if isinstance(body, dict) else None

    reader = request.app.state.vault
    writer = request.app.state.vault_writer

    if edited is not None:
        pending_path = f"memory/pending/memory-updates_{date}.md"
        if not reader.exists(pending_path):
            raise HTTPException(status_code=404, detail=f"No pending updates for {date}")
        doc = reader.read(pending_path)
        # Rebuild the file preserving frontmatter but replacing body
        import frontmatter
        post = frontmatter.Post(edited, handler=None, **(doc.metadata or {}))
        writer.write(pending_path, frontmatter.dumps(post), trust_reason="memory_edited_before_approve")

    # Capture the pending file's correlation_id BEFORE approve_memory_updates
    # mutates the frontmatter (status pending→approved). Fed into the
    # evolution layer after approval so the corresponding run's posterior
    # gets the user's positive signal.
    _pending_corr_id = ""
    try:
        _pending = reader.read(f"memory/pending/memory-updates_{date}.md")
        _pending_corr_id = str((_pending.metadata or {}).get("correlation_id", ""))
    except Exception:
        pass

    engine = request.app.state.memory_engine
    approved = engine.approve_memory_updates(date)
    if not approved:
        raise HTTPException(status_code=404, detail=f"No pending updates for {date}")

    # Record feedback for relationship learning
    activity = getattr(request.app.state, "activity_logger", None)
    if activity:
        activity.record_feedback("approval", f"Memory updates for {date}")

    review_history = getattr(request.app.state, "review_history", None)
    if review_history:
        await review_history.record("memory", date, "approved")

    # Evolution feedback loop — closes the cycle from user approval to
    # reflex posterior. Silently no-ops when no correlation_id is
    # stamped (legacy pending files) or no run matches (memory
    # consolidation hasn't migrated to invoke_skill yet).
    evolution = getattr(request.app.state, "evolution", None)
    if evolution is not None and _pending_corr_id:
        await evolution.record_feedback_by_correlation(_pending_corr_id, 1.0)

    return {"status": "approved", "date": date}


@router.post("/pending/{date}/reject")
async def reject_memory_updates(request: Request, date: str) -> dict:
    """Reject pending memory updates for a date. Marks them as rejected."""
    reader = request.app.state.vault
    writer = request.app.state.vault_writer

    pending_path = f"memory/pending/memory-updates_{date}.md"
    if not reader.exists(pending_path):
        raise HTTPException(status_code=404, detail=f"No pending updates for {date}")

    try:
        body = await request.json()
    except Exception:
        body = {}
    reason = body.get("reason", "") if isinstance(body, dict) else ""

    # Mark as rejected
    doc = reader.read(pending_path)
    content = doc.raw.replace("status: pending", "status: rejected")
    if reason:
        content = content.replace("---\n", f"---\nrejection_reason: {reason}\n", 1)
    writer.write(pending_path, content, trust_reason="memory_rejected")

    activity = getattr(request.app.state, "activity_logger", None)
    if activity:
        activity.record_feedback("rejection", f"Memory updates for {date}" + (f" — {reason}" if reason else ""))

    review_history = getattr(request.app.state, "review_history", None)
    if review_history:
        await review_history.record("memory", date, "rejected", reason=reason)

    # Evolution feedback loop — rejection signal (score 0.0).
    evolution = getattr(request.app.state, "evolution", None)
    pending_corr_id = str((doc.metadata or {}).get("correlation_id", ""))
    if evolution is not None and pending_corr_id:
        await evolution.record_feedback_by_correlation(pending_corr_id, 0.0)

    return {"status": "rejected", "date": date}


@router.post("/pending/{date}/unapprove")
async def unapprove_memory_updates(request: Request, date: str) -> dict:
    """Revert an approved memory update — marks it as pending again.

    Note: this does NOT remove the facts from MEMORY.md (that would require
    memory correction). It resets the pending file status so the user can
    review and decide again.
    """
    reader = request.app.state.vault
    writer = request.app.state.vault_writer

    pending_path = f"memory/pending/memory-updates_{date}.md"
    if not reader.exists(pending_path):
        raise HTTPException(status_code=404, detail=f"No pending updates for {date}")

    doc = reader.read(pending_path)
    content = doc.raw.replace("status: approved", "status: pending")
    writer.write(pending_path, content, trust_reason="memory_unapproved")

    activity = getattr(request.app.state, "activity_logger", None)
    if activity:
        activity.log("memory", "unapproved", f"Memory updates for {date} reverted to pending",
                     importance="medium")

    review_history = getattr(request.app.state, "review_history", None)
    if review_history:
        await review_history.record("memory", date, "unapproved")

    return {"status": "unapproved", "date": date}


@router.get("/health")
async def memory_health(request: Request) -> dict:
    """Comprehensive memory health check.

    Returns metrics, issues detected, and an overall health score (0.0-1.0).
    """
    graph = request.app.state.knowledge_graph
    engine = request.app.state.memory_engine

    # Gather stats
    stats = graph.get_stats()
    spo = stats.get("spo", {})
    active = spo.get("active_triples", 0)
    decayed = spo.get("decayed_triples", 0)
    superseded = spo.get("superseded_triples", 0)
    total_triples = active + decayed + superseded

    # Detect orphaned entities (in registry but no active triples reference them)
    orphaned_entities: list[str] = []
    active_slugs: set[str] = set()
    for t in graph._triples:
        if t.get("status") == "active":
            active_slugs.add(t.get("subject", ""))
            obj = t.get("object")
            if obj:
                active_slugs.add(obj)
    for slug, ent in graph._registry.entities.items():
        if ent.get("status") != "decayed" and slug not in active_slugs:
            orphaned_entities.append(slug)

    # Detect orphaned triples (reference non-existent entities)
    orphaned_triples: list[int] = []
    entity_slugs = set(graph._registry.entities.keys())
    for t in graph._triples:
        if t.get("status") != "active":
            continue
        if t["subject"] not in entity_slugs or t.get("object") and t["object"] not in entity_slugs:
            orphaned_triples.append(t["id"])

    # Contradictions
    contradictions = graph.get_contradictions()

    # Daily memory coverage (last 7 days)
    dates = engine.list_memory_dates(limit=7)
    from datetime import timedelta

    from agntspace.infra.timezone import now_local
    expected_dates = [(now_local() - timedelta(days=i)).strftime("%Y-%m-%d") for i in range(7)]
    missing_days = [d for d in expected_dates if d not in dates]

    # Compute health score
    issues: list[dict] = []
    score = 1.0

    if orphaned_entities:
        issues.append({"type": "orphaned_entities", "count": len(orphaned_entities), "severity": "low", "items": orphaned_entities[:10]})
        score -= min(0.1, len(orphaned_entities) * 0.01)

    if orphaned_triples:
        issues.append({"type": "orphaned_triples", "count": len(orphaned_triples), "severity": "medium", "items": orphaned_triples[:10]})
        score -= min(0.15, len(orphaned_triples) * 0.02)

    if contradictions:
        issues.append({"type": "contradictions", "count": len(contradictions), "severity": "medium", "items": contradictions[:5]})
        score -= min(0.15, len(contradictions) * 0.03)

    if missing_days:
        issues.append({"type": "missing_daily_memories", "count": len(missing_days), "severity": "low", "items": missing_days})
        score -= min(0.1, len(missing_days) * 0.02)

    stale_ratio = decayed / total_triples if total_triples else 0
    if stale_ratio > 0.5:
        issues.append({"type": "high_decay_ratio", "ratio": round(stale_ratio, 2), "severity": "medium"})
        score -= 0.1

    authority = spo.get("authority_breakdown", {})
    explicit_count = authority.get("explicit", 0)
    verification_ratio = explicit_count / active if active else 0
    if active > 10 and verification_ratio < 0.1:
        issues.append({"type": "low_verification_ratio", "ratio": round(verification_ratio, 2), "severity": "low"})
        score -= 0.05

    # Orphaned justification dependencies (justified_by refs to non-existent triples)
    all_triple_ids = {t["id"] for t in graph._triples}
    orphaned_deps = []
    for t in graph._triples:
        if t.get("status") != "active":
            continue
        for jb_id in t.get("justified_by", []):
            if jb_id not in all_triple_ids:
                orphaned_deps.append({"triple_id": t["id"], "missing": jb_id})
    if orphaned_deps:
        issues.append({
            "type": "orphaned_dependencies", "count": len(orphaned_deps),
            "severity": "high", "items": orphaned_deps[:10],
        })
        score -= min(0.2, len(orphaned_deps) * 0.03)

    score = max(0.0, round(score, 2))

    status = "healthy" if score >= 0.8 else "degraded" if score >= 0.5 else "critical"

    return {
        "status": status,
        "score": score,
        "stats": stats,
        "issues": issues,
        "summary": {
            "active_triples": active,
            "decayed_triples": decayed,
            "superseded_triples": superseded,
            "total_entities": spo.get("entities", 0),
            "orphaned_entities": len(orphaned_entities),
            "orphaned_triples": len(orphaned_triples),
            "contradictions": len(contradictions),
            "missing_daily_memories": len(missing_days),
            "stale_ratio": round(stale_ratio, 2),
            "verification_ratio": round(verification_ratio, 2),
        },
    }


@router.post("/health/repair")
async def memory_repair(request: Request) -> dict:
    """Auto-repair detected memory issues.

    - Removes orphaned triples (reference non-existent entities)
    - Runs decay evaluation
    - Resolves contradictions via authority ranking
    - Rebuilds graph indices
    """
    graph = request.app.state.knowledge_graph
    repaired: dict[str, int] = {}

    # 1. Remove orphaned triples (reference non-existent entities)
    entity_slugs = set(graph._registry.entities.keys())
    clean_triples = []
    removed_ids: set[int] = set()
    for t in graph._triples:
        if t.get("status") == "active":
            subj_ok = t["subject"] in entity_slugs
            obj_ok = not t.get("object") or t["object"] in entity_slugs
            if not (subj_ok and obj_ok):
                removed_ids.add(t["id"])
                continue
        clean_triples.append(t)
    graph._triples = clean_triples
    repaired["orphaned_triples_removed"] = len(removed_ids)

    # 1b. Clean orphaned justified_by references (triples whose justifications were removed)
    all_triple_ids = {t["id"] for t in graph._triples}
    deps_cleaned = 0
    for t in graph._triples:
        justified_by = t.get("justified_by", [])
        valid = [jb for jb in justified_by if jb in all_triple_ids]
        if len(valid) < len(justified_by):
            t["justified_by"] = valid
            deps_cleaned += 1
            # If an inferred triple lost ALL its justifications, mark uncertain
            if not valid and t.get("authority") == "inferred" and t.get("epistemic_status") not in ("verified", "superseded", "retracted"):
                t["epistemic_status"] = "uncertain"
    repaired["orphaned_dependencies_cleaned"] = deps_cleaned

    # 2. Run decay evaluation
    decay_result = graph.evaluate_decay()
    repaired["entities_decayed"] = decay_result["entities_decayed"]
    repaired["triples_decayed"] = decay_result["triples_decayed"]

    # 3. Resolve contradictions by marking lower-authority/confidence facts as superseded
    contradictions = graph.get_contradictions()
    resolved = 0
    for c in contradictions:
        existing = c.get("existing")
        incoming = c.get("incoming") or c.get("new")
        if not existing or not incoming:
            continue
        # Keep the higher-authority one
        auth_rank = {"explicit": 3, "system": 2, "inferred": 1}
        e_rank = auth_rank.get(existing.get("authority", "inferred"), 1)
        i_rank = auth_rank.get(incoming.get("authority", "inferred"), 1)
        loser_id = incoming["id"] if e_rank >= i_rank else existing["id"]
        for t in graph._triples:
            if t["id"] == loser_id and t["status"] == "active":
                t["status"] = "superseded"
                resolved += 1
                break
    repaired["contradictions_resolved"] = resolved

    # 4. Save and rebuild
    if resolved:
        graph._dirty = True
        graph.save()

    graph.build()
    repaired["graph_rebuilt"] = True

    log.info("Memory repair: %s", repaired)
    return {"repaired": repaired}


@router.post("/pending/batch-approve")
async def batch_approve_memory(request: Request) -> dict:
    """Batch approve pending memory updates.

    Body: ``{"dates": ["2026-04-15", "2026-04-16"]}``
    If ``dates`` is empty or omitted, approves ALL pending items.
    """
    try:
        body = await request.json()
    except Exception:
        body = {}
    dates = body.get("dates", []) if isinstance(body, dict) else []

    engine = request.app.state.memory_engine
    pending = engine.list_pending_memory(status="pending")

    if dates:
        pending = [p for p in pending if p.get("date") in dates]

    # Collect correlation_ids BEFORE approving (which rewrites frontmatter).
    reader = request.app.state.vault
    date_corr_ids: list[tuple[str, str]] = []
    for item in pending:
        date = item.get("date", "")
        if not date:
            continue
        try:
            _d = reader.read(f"memory/pending/memory-updates_{date}.md")
            cid = str((_d.metadata or {}).get("correlation_id", ""))
            if cid:
                date_corr_ids.append((date, cid))
        except Exception:
            pass

    approved = 0
    skipped = 0
    errors: list[str] = []
    approved_dates: list[str] = []

    for item in pending:
        date = item.get("date", "")
        try:
            ok = engine.approve_memory_updates(date)
            if ok:
                approved += 1
                approved_dates.append(date)
            else:
                skipped += 1
        except Exception as exc:
            errors.append(f"{date}: {exc}")

    # Record ONE batch feedback event
    activity = getattr(request.app.state, "activity_logger", None)
    if activity and approved:
        activity.record_feedback("approval", f"Batch approved {approved} memory updates")

    # Record in review history
    review_history = getattr(request.app.state, "review_history", None)
    if review_history and approved:
        await review_history.record(
            "memory", ",".join(approved_dates), "batch_approved",
            details={"approved": approved, "skipped": skipped},
        )

    # Evolution feedback — one positive signal per approved date whose
    # pending file had a correlation_id stamped.
    evolution = getattr(request.app.state, "evolution", None)
    if evolution is not None:
        for date, cid in date_corr_ids:
            if date in approved_dates:
                await evolution.record_feedback_by_correlation(cid, 1.0)

    return {"approved": approved, "skipped": skipped, "errors": errors}


# Wildcard route LAST — must be after all specific paths.
# Regex constrains to YYYY-MM-DD so it won't swallow named sub-routes
# like /memory/affective-map or /memory/dream-review that live on other routers.
@router.get("/{date:path}")
async def get_memory(request: Request, date: str) -> dict:
    """Get memory file for a specific date (YYYY-MM-DD)."""
    import re
    if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
        raise HTTPException(status_code=400, detail=f"Invalid date format: {date!r} — expected YYYY-MM-DD")
    engine = request.app.state.memory_engine
    content = engine.read_daily_memory(date)
    return {"date": date, "content": content, "exists": content is not None}
