# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Activity monitoring, metrics, and code graph routes."""

from __future__ import annotations

import json
import logging
import os
import threading
from datetime import datetime, timezone
from pathlib import Path

from flask import Blueprint, Response, current_app, jsonify, request, send_file

from ..auth import require_auth
from ..helpers import get_agent, get_service_manager, DATA_DIR, safe_error

logger = logging.getLogger(__name__)

bp = Blueprint("activity", __name__)


# ============================================================
# Prometheus Metrics Endpoint
# ============================================================


@bp.route("/metrics")
def prometheus_metrics():
    """Expose metrics in Prometheus text exposition format."""
    # Optionally gate behind auth
    if os.environ.get("FAMILIAR_METRICS_AUTH"):
        from functools import wraps

        auth = request.headers.get("Authorization", "")
        if not auth:
            return "Unauthorized", 401

    try:
        from familiar.core.metrics import get_metrics_collector

        mc = get_metrics_collector()
        lines = []

        # Tool metrics
        for tm in mc.get_all_tool_metrics().values():
            safe = tm.tool_name.replace("-", "_").replace(".", "_").replace("/", "_")
            lines.append(f"familiar_tool_{safe}_calls_total {tm.total_calls}")
            lines.append(f"familiar_tool_{safe}_success_total {tm.successful_calls}")
            lines.append(f"familiar_tool_{safe}_duration_ms_avg {tm.avg_duration_ms:.2f}")
            lines.append(f"familiar_tool_{safe}_duration_ms_p95 {tm.p95_duration_ms:.2f}")
            lines.append(f"familiar_tool_{safe}_timeouts_total {tm.timeout_count}")

        # LLM metrics
        for lm in mc.get_all_llm_metrics().values():
            safe = (
                f"{lm.provider}_{lm.model}".replace("-", "_")
                .replace(".", "_")
                .replace("/", "_")
                .replace(":", "_")
            )
            lines.append(f"familiar_llm_{safe}_calls_total {lm.total_calls}")
            lines.append(f"familiar_llm_{safe}_success_total {lm.successful_calls}")
            lines.append(f"familiar_llm_{safe}_tokens_in_total {lm.total_prompt_tokens}")
            lines.append(f"familiar_llm_{safe}_tokens_out_total {lm.total_completion_tokens}")
            lines.append(f"familiar_llm_{safe}_cost_usd_total {lm.total_cost_usd:.6f}")
            lines.append(f"familiar_llm_{safe}_duration_ms_avg {lm.avg_duration_ms:.2f}")
            lines.append(f"familiar_llm_{safe}_duration_ms_p95 {lm.p95_duration_ms:.2f}")
            lines.append(f"familiar_llm_{safe}_rate_limits_total {lm.rate_limit_errors}")

        body = "\n".join(lines) + "\n"
        return app.response_class(body, mimetype="text/plain; version=0.0.4; charset=utf-8")
    except Exception:
        return app.response_class("", mimetype="text/plain")


# ============================================================
# Activity Monitor Endpoints
# ============================================================


@bp.route("/api/activity/metrics")
@require_auth
def api_activity_metrics():
    """Get tool and LLM performance metrics with alerts."""
    try:
        from familiar.core.metrics import get_metrics_collector

        mc = get_metrics_collector()

        # Tool metrics sorted by call count
        tools = []
        for tm in sorted(
            mc.get_all_tool_metrics().values(), key=lambda t: t.total_calls, reverse=True
        ):
            tools.append(
                {
                    "tool_name": tm.tool_name,
                    "total_calls": tm.total_calls,
                    "success_rate": round(tm.success_rate, 4),
                    "avg_duration_ms": round(tm.avg_duration_ms, 1),
                    "p95_duration_ms": round(tm.p95_duration_ms, 1),
                    "timeout_count": tm.timeout_count,
                    "last_error": tm.last_error,
                    "recent_durations": [round(d, 1) for d in tm.durations[-20:]],
                    "category": tm.category,
                    "tags": tm.tags,
                    "skill": tm.skill,
                }
            )

        # LLM provider metrics — merge in-memory (current session) with
        # persistent analytics.db so local Ollama calls always appear.
        llm_providers = []
        _seen_models = set()
        for lm in sorted(
            mc.get_all_llm_metrics().values(), key=lambda m: m.total_calls, reverse=True
        ):
            _seen_models.add(lm.model)
            llm_providers.append(
                {
                    "provider": lm.provider,
                    "model": lm.model,
                    "total_calls": lm.total_calls,
                    "success_rate": round(lm.success_rate, 4),
                    "avg_duration_ms": round(lm.avg_duration_ms, 1),
                    "p95_duration_ms": round(lm.p95_duration_ms, 1),
                    "total_prompt_tokens": lm.total_prompt_tokens,
                    "total_completion_tokens": lm.total_completion_tokens,
                    "total_cost_usd": round(lm.total_cost_usd, 6),
                    "rate_limit_errors": lm.rate_limit_errors,
                    "last_error": lm.last_error,
                    "tool_call_rate": round(lm.tool_call_rate, 4)
                    if lm.tool_call_rate is not None
                    else None,
                    "empty_tool_retries": lm.empty_tool_retries,
                    "calls_with_tools": lm.calls_with_tools_provided,
                    "tool_calls_produced": lm.total_tool_calls_produced,
                    "recent_durations": [round(d, 1) for d in lm.durations[-20:]],
                }
            )

        # Merge persistent analytics.db data for models not in current session
        try:
            from familiar.core.analytics import get_analytics_store

            _astore = get_analytics_store()
            with _astore._connect() as _aconn:
                _db_models = _aconn.execute(
                    """
                    SELECT model,
                           COUNT(*) as calls,
                           COALESCE(SUM(tokens_in), 0) as tok_in,
                           COALESCE(SUM(tokens_out), 0) as tok_out,
                           COALESCE(SUM(cost_usd), 0.0) as cost,
                           COALESCE(AVG(latency_ms), 0) as avg_lat
                    FROM events
                    WHERE event_type = 'message'
                      AND model != '' AND model NOT LIKE '%Mock%'
                    GROUP BY model
                    ORDER BY calls DESC
                    """
                ).fetchall()
                for row in _db_models:
                    _m = row["model"]
                    if _m in _seen_models:
                        continue
                    _prov = "ollama"
                    if "claude" in _m.lower() or "sonnet" in _m.lower() or "haiku" in _m.lower():
                        _prov = "anthropic"
                    elif "gpt" in _m.lower():
                        _prov = "openai"
                    elif "gemini" in _m.lower():
                        _prov = "google"
                    llm_providers.append(
                        {
                            "provider": _prov,
                            "model": _m,
                            "total_calls": row["calls"],
                            "success_rate": 1.0,
                            "avg_duration_ms": round(row["avg_lat"] or 0, 1),
                            "p95_duration_ms": 0,
                            "total_prompt_tokens": row["tok_in"] or 0,
                            "total_completion_tokens": row["tok_out"] or 0,
                            "total_cost_usd": round(row["cost"] or 0, 6),
                            "rate_limit_errors": 0,
                            "last_error": None,
                            "tool_call_rate": None,
                            "empty_tool_retries": 0,
                            "calls_with_tools": 0,
                            "tool_calls_produced": 0,
                            "recent_durations": [],
                        }
                    )
        except Exception as _merge_err:
            logger.warning("Activity metrics DB merge failed: %s", _merge_err)

        # Alerts
        alerts = [a.to_dict() for a in mc.get_alerts()]

        # Rollup KPIs
        tool_summary = mc.get_tool_summary()
        llm_summary = mc.get_llm_summary()

        # Compute overall tool call rate across all providers
        all_llm = mc.get_all_llm_metrics()
        _tcr_calls = sum(m.calls_with_tools_provided for m in all_llm.values())
        _tcr_produced = sum(m.total_tool_calls_produced for m in all_llm.values())
        _overall_tcr = (_tcr_produced / _tcr_calls) if _tcr_calls > 0 else None
        _total_retries = sum(m.empty_tool_retries for m in all_llm.values())

        # Category aggregation (Sprint 4)
        categories = mc.get_metrics_by_category()
        slowest = mc.get_slowest_categories(top_n=1)
        slowest_cat = {"name": slowest[0][0], "avg_ms": slowest[0][1]} if slowest else None

        return jsonify(
            {
                "tools": tools,
                "llm_providers": llm_providers,
                "alerts": alerts,
                "categories": categories,
                "kpi": {
                    "total_tool_calls": tool_summary.get("total_calls", 0),
                    "tool_success_rate": round(tool_summary.get("overall_success_rate") or 1.0, 4),
                    "total_tokens": llm_summary.get("total_tokens", 0),
                    "total_cost_usd": round(llm_summary.get("total_cost_usd", 0.0), 6),
                    "tool_call_rate": round(_overall_tcr, 4) if _overall_tcr is not None else None,
                    "empty_tool_retries": _total_retries,
                    "slowest_category": slowest_cat,
                },
            }
        )
    except Exception as _act_err:
        logger.exception("Activity metrics failed: %s", _act_err)
        return jsonify(
            {
                "tools": [],
                "llm_providers": [],
                "alerts": [],
                "kpi": {
                    "total_tool_calls": 0,
                    "tool_success_rate": 1.0,
                    "total_tokens": 0,
                    "total_cost_usd": 0.0,
                },
            }
        )


@bp.route("/api/activity/errors")
@require_auth
def api_activity_errors():
    """Get recent error events from analytics store."""
    try:
        from familiar.core.analytics import EventType, get_analytics_store

        import sqlite3 as _sql3

        db_path = Path.home() / ".familiar" / "data" / "analytics.db"
        errors = []
        if db_path.exists():
            conn = _sql3.connect(str(db_path))
            conn.row_factory = _sql3.Row
            rows = conn.execute(
                "SELECT timestamp, skill, channel, model, error_message, latency_ms, metadata "
                "FROM events "
                "WHERE (error_message IS NOT NULL AND error_message != '') "
                "   OR success = 0 "
                "   OR event_type = 'error' "
                "ORDER BY timestamp DESC LIMIT 50"
            ).fetchall()
            conn.close()
            for r in rows:
                import json as _j

                _meta = {}
                try:
                    _meta = _j.loads(r["metadata"]) if r["metadata"] else {}
                except Exception:
                    pass
                errors.append(
                    {
                        "timestamp": r["timestamp"] or "",
                        "skill": r["skill"] or "",
                        "channel": r["channel"] or "",
                        "model": r["model"] or "",
                        "error_message": r["error_message"] or "",
                        "latency_ms": r["latency_ms"],
                        "request_id": _meta.get("request_id", ""),
                    }
                )
        return jsonify({"errors": errors})
    except Exception:
        logger.debug("Activity errors unavailable", exc_info=True)
        return jsonify({"errors": []})


@bp.route("/api/activity/skill-costs")
@require_auth
def api_activity_skill_costs():
    """Get per-skill cost and usage breakdown."""
    try:
        from datetime import timedelta

        from familiar.core.analytics import get_analytics_store

        store = get_analytics_store()
        days = int(request.args.get("days", 30))
        start = datetime.now(timezone.utc) - timedelta(days=days)
        summary = store.get_summary(start=start)
        skills = []
        for name, data in sorted(
            summary.by_skill.items(), key=lambda x: x[1]["cost"], reverse=True
        ):
            skills.append(
                {
                    "skill": name,
                    "calls": data["calls"],
                    "cost": round(data["cost"], 6),
                    "avg_latency": round(data["avg_latency"], 1),
                }
            )
        return jsonify({"skills": skills})
    except Exception:
        logger.debug("Skill costs unavailable", exc_info=True)
        return jsonify({"skills": []})


@bp.route("/api/activity/timeseries")
@require_auth
def api_activity_timeseries():
    """Get daily cost and token time-series for charting."""
    try:
        from datetime import timedelta

        from familiar.core.analytics import get_analytics_store

        store = get_analytics_store()
        days = int(request.args.get("days", 30))
        start = datetime.now(timezone.utc) - timedelta(days=days)
        costs = store.get_daily_costs(start=start)
        tokens = store.get_daily_tokens(start=start)
        return jsonify({"costs": costs, "tokens": tokens})
    except Exception:
        logger.debug("Timeseries unavailable", exc_info=True)
        return jsonify({"costs": [], "tokens": []})


@bp.route("/api/activity/kpis")
@require_auth
def api_activity_kpis():
    """Get persistent KPIs from analytics.db (today/month/all-time)."""
    try:
        from familiar.core.analytics import get_analytics_store

        store = get_analytics_store()
        return jsonify(store.get_period_kpis())
    except Exception:
        logger.debug("KPIs unavailable", exc_info=True)
        return jsonify(
            {
                "today": {"calls": 0, "tokens_in": 0, "tokens_out": 0, "cost": 0},
                "month": {"calls": 0, "tokens_in": 0, "tokens_out": 0, "cost": 0},
                "all_time": {"calls": 0, "tokens_in": 0, "tokens_out": 0, "cost": 0},
                "active_model": "",
            }
        )


@bp.route("/api/activity/call-log")
@require_auth
def api_activity_call_log():
    """Get paginated LLM call log with trigger classification."""
    try:
        from familiar.core.analytics import get_analytics_store

        store = get_analytics_store()
        page = int(request.args.get("page", 1))
        per_page = int(request.args.get("per_page", 50))
        start = request.args.get("start")
        end = request.args.get("end")
        trigger = request.args.get("trigger")

        calls, total = store.get_call_log(
            page=page, per_page=per_page, start=start, end=end, trigger=trigger
        )
        pages = max(1, (total + per_page - 1) // per_page)
        return jsonify({"calls": calls, "total": total, "page": page, "pages": pages})
    except Exception:
        logger.debug("Call log unavailable", exc_info=True)
        return jsonify({"calls": [], "total": 0, "page": 1, "pages": 1})


@bp.route("/api/activity/monthly-spend")
@require_auth
def api_activity_monthly_spend():
    """Get monthly spend analysis with proactive vs interactive breakdown."""
    try:
        from familiar.core.analytics import get_analytics_store

        store = get_analytics_store()
        return jsonify(store.get_monthly_spend())
    except Exception:
        logger.debug("Monthly spend unavailable", exc_info=True)
        return jsonify(
            {
                "total": 0,
                "daily_avg": 0,
                "projected": 0,
                "proactive_cost": 0,
                "interactive_cost": 0,
                "last_month_total": 0,
                "change_pct": 0,
                "days_elapsed": 0,
                "days_in_month": 30,
            }
        )


@bp.route("/api/activity/lifecycle")
@require_auth
def api_activity_lifecycle():
    """Get recent agent lifecycle events (Ruflo-inspired event bus)."""
    try:
        agent = get_agent()
        emitter = getattr(agent, "_lifecycle_emitter", None)
        if not emitter:
            return jsonify({"events": [], "message": "No recent lifecycle events"})
        history = emitter.get_history()
        events = [
            {
                "type": e.type.value if hasattr(e.type, "value") else str(e.type),
                "timestamp": e.timestamp.isoformat() if e.timestamp else "",
                "content": e.content or "",
                "data": e.data or {},
                "sequence": e.sequence,
            }
            for e in history[-50:]  # Last 50 events
        ]
        return jsonify({"events": events, "total": len(history)})
    except Exception as e:
        return jsonify({"events": [], "error": safe_error(e)})


@bp.route("/api/code-graph")
@require_auth
def api_code_graph():
    """Return the code structure dependency graph for visualization."""
    try:
        from familiar.core.code_graph import build_code_graph

        depth = request.args.get("depth", 4, type=int)
        include_tests = request.args.get("tests", "false").lower() == "true"
        graph = build_code_graph(max_depth=min(depth, 5), include_tests=include_tests)
        return jsonify(graph)
    except Exception as e:
        logger.exception("Code graph failed: %s", e)
        return jsonify({"nodes": [], "links": [], "stats": {}, "error": safe_error(e)})


@bp.route("/api/code-graph/impact")
@require_auth
def api_code_graph_impact():
    """Compute the impact radius for a module (what breaks if it changes)."""
    try:
        from familiar.core.code_graph import compute_impact_radius

        module_id = request.args.get("module", "")
        if not module_id:
            return jsonify({"error": "module parameter required"}), 400
        result = compute_impact_radius(module_id)
        return jsonify(result)
    except Exception as e:
        return jsonify({"error": safe_error(e)}), 500


@bp.route("/api/filesystem/scan")
@require_auth
def api_filesystem_scan():
    """Scan a directory tree for filesystem visualization."""
    try:
        from familiar.core.fs_graph import scan_directory

        root = request.args.get("root", "~")
        depth = request.args.get("depth", 3, type=int)
        max_nodes = request.args.get("max_nodes", 800, type=int)
        min_size = request.args.get("min_size", 0, type=int)

        # Security: restrict to home directory and common paths
        from pathlib import Path
        resolved = Path(root).expanduser().resolve()
        home = Path.home().resolve()
        allowed = [home, Path("/tmp"), Path("/var/log")]
        if not any(str(resolved).startswith(str(a)) for a in allowed):
            return jsonify({"error": "Scan restricted to home directory"}), 403

        result = scan_directory(
            root=root,
            max_depth=min(depth, 5),
            max_nodes=min(max_nodes, 2000),
            min_size=min_size,
        )
        return jsonify(result)
    except Exception as e:
        logger.exception("Filesystem scan failed: %s", e)
        return jsonify({"nodes": [], "children": {}, "stats": {"error": safe_error(e)}})


@bp.route("/api/activity/health")
@require_auth
def api_activity_health():
    """Get system health: resources, dependencies, circuit breakers."""
    result = {
        "status": "unknown",
        "uptime_seconds": 0,
        "dependencies": [],
        "resources": {},
        "circuits": {},
        "routing": {},
    }
    try:
        from familiar.core.health import get_health_checker

        hc = get_health_checker()
        health = hc.check_health(include_resources=True)
        hdict = health.to_dict()
        result["status"] = hdict.get("status", "unknown")
        result["uptime_seconds"] = hdict.get("uptime_seconds", 0)
        result["resources"] = hdict.get("resources", {})
        result["dependencies"] = hdict.get("dependencies", [])
    except Exception:
        logger.debug("Health status unavailable", exc_info=True)

    try:
        from familiar.core.resilience import get_all_circuit_status

        result["circuits"] = get_all_circuit_status()
    except Exception:
        logger.debug("Circuit breaker status unavailable", exc_info=True)

    # Routing context: active provider + fallback chain + rate limits
    try:
        agent = get_agent()
        result["routing"]["active_provider"] = agent.provider.name
        result["routing"]["active_provider_key"] = getattr(agent.provider, "provider_key", "")
        result["routing"]["active_model"] = getattr(agent.provider, "model_name", "")
        if hasattr(agent, "providers") and agent.providers.model_router:
            router = agent.providers.model_router
            result["routing"]["fallback_chain"] = list(getattr(router, "_fallback_chain", []))
            router_models = router.get_model_names()
            # Merge in all Ollama models (catalog + installed)
            try:
                from familiar.channels.connect_wizard._llm import OLLAMA_MODEL_CATALOG

                catalog_set = {f"ollama/{tag}" for tag, *_ in OLLAMA_MODEL_CATALOG}
            except ImportError:
                catalog_set = set()
            try:
                import httpx as _httpx

                resp = _httpx.get("http://localhost:11434/api/tags", timeout=3)
                installed_set = {
                    f"ollama/{m.get('name', '')}"
                    for m in resp.json().get("models", [])
                    if m.get("name")
                }
            except Exception:
                installed_set = set()
            all_models = set(router_models) | catalog_set | installed_set
            result["routing"]["available_models"] = sorted(all_models)
        if hasattr(agent, "providers"):
            tracker = agent.providers.capacity_tracker
            result["rate_limits"] = tracker.get_status()
    except Exception:
        logger.debug("Routing info unavailable", exc_info=True)

    # Cache stats — session cache + provider response cache
    try:
        agent = get_agent()
        cache_info = {}
        if hasattr(agent, "sessions") and hasattr(agent.sessions, "get_cache_stats"):
            cache_info["sessions"] = agent.sessions.get_cache_stats()
        if hasattr(agent, "providers") and hasattr(agent.providers, "_response_cache"):
            cache_info["provider_responses"] = agent.providers._response_cache.stats()
        if cache_info:
            result["caches"] = cache_info
    except Exception:
        logger.debug("Cache stats unavailable", exc_info=True)

    return jsonify(result)


