"""Optional REST API for NEUROVAULT."""

from __future__ import annotations

import logging
import hmac
import os
from typing import Any, Optional

try:
    from fastapi import Depends, FastAPI, Header, HTTPException
    from fastapi.responses import PlainTextResponse
except Exception:  # pragma: no cover
    FastAPI = None  # type: ignore
    Depends = None  # type: ignore
    Header = None  # type: ignore
    PlainTextResponse = None  # type: ignore
    HTTPException = Exception  # type: ignore

from neurovault.engine import NeurovaultEngine
from neurovault.graphql_api import GraphQLService
from neurovault.observability.prometheus import PrometheusExporter
from neurovault.security.rbac import Permission

logger = logging.getLogger(__name__)


class _ApiState:
    def __init__(self, engine: NeurovaultEngine):
        self.engine = engine


def create_app(vault_name: str = "default"):
    """Create FastAPI app exposing NEUROVAULT endpoints."""
    if FastAPI is None:  # pragma: no cover
        raise ImportError("Install with: pip install neurovault[server]")

    app = FastAPI(title="NEUROVAULT API", version="0.3.1")
    state = _ApiState(NeurovaultEngine.open(vault_name))
    graphql = GraphQLService(state.engine)
    metrics = PrometheusExporter(state.engine)
    bootstrap_token = os.environ.get("NEUROVAULT_BOOTSTRAP_TOKEN", "").strip()

    def _auth(required: str):
        def _dep(
            authorization: Optional[str] = Header(default=None),
            bootstrap_header: Optional[str] = Header(default=None, alias="X-Neurovault-Bootstrap-Token"),
        ):
            users = state.engine.list_users()
            if not users:
                if not bootstrap_token:
                    raise HTTPException(
                        status_code=503,
                        detail="vault has no users; set NEUROVAULT_BOOTSTRAP_TOKEN to bootstrap admin",
                    )
                if not bootstrap_header:
                    raise HTTPException(status_code=401, detail="missing bootstrap token")
                if not hmac.compare_digest(bootstrap_header, bootstrap_token):
                    raise HTTPException(status_code=403, detail="invalid bootstrap token")
                return {"username": "bootstrap", "permissions": {Permission.ADMIN, Permission.MANAGE}}

            if not authorization or not authorization.startswith("Bearer "):
                raise HTTPException(status_code=401, detail="missing bearer token")
            token = authorization.split(" ", 1)[1]
            user = state.engine.authenticate_token(token)
            if user is None:
                raise HTTPException(status_code=403, detail="invalid token")
            if not state.engine.check_token_permission(token, required):
                raise HTTPException(status_code=403, detail="insufficient permission")
            return user

        return _dep

    @app.get("/api/v1/health")
    def health() -> dict[str, str]:
        return {"status": "ok", "service": "neurovault"}

    @app.get("/api/v1/health/ready")
    def health_ready() -> dict[str, Any]:
        payload = state.engine.stats()
        return {
            "status": "ready",
            "memory_count": int(payload.get("memory_count", 0)),
            "entity_count": int(payload.get("entity_count", 0)),
            "daemon_running": bool(payload.get("daemon_running", False)),
        }

    @app.get("/metrics", response_class=PlainTextResponse)
    def prometheus_metrics():
        return metrics.generate_metrics()

    @app.get("/api/v1/metrics", response_class=PlainTextResponse)
    def prometheus_metrics_v1():
        return metrics.generate_metrics()

    @app.post("/api/v1/graphql")
    def graphql_query(payload: dict[str, Any]):
        query = str(payload.get("query", "")).strip()
        if not query:
            raise HTTPException(status_code=400, detail="query is required")
        variables = payload.get("variables", {})
        if variables is not None and not isinstance(variables, dict):
            raise HTTPException(status_code=400, detail="variables must be an object")
        result = graphql.execute(query, variables=variables or {})
        if "errors" in result:
            return result
        return result

    @app.post("/api/v1/ingest")
    def ingest(payload: dict[str, Any]) -> dict[str, Any]:
        content = str(payload.get("content", "")).strip()
        if not content:
            raise HTTPException(status_code=400, detail="content is required")
        memory = state.engine.ingest(
            content=content,
            source=str(payload.get("source", "api")),
            importance=float(payload.get("importance", 0.5)),
            metadata=payload.get("metadata", {}),
        )
        if memory is None:
            raise HTTPException(status_code=403, detail="blocked by privacy filter")
        return {"id": memory.id.hex(), "content": memory.content, "source": memory.source}

    @app.get("/api/v1/recall")
    def recall(q: str, mode: str = "multi", limit: int = 10, since: Optional[int] = None):
        results = state.engine.recall(q, limit=limit, mode=mode, since=since)
        return [
            {
                "id": r.memory.id.hex(),
                "content": r.memory.content,
                "score": r.score,
                "type": r.match_type,
            }
            for r in results
        ]

    @app.get("/api/v1/predictions")
    def predictions():
        results = state.engine.predict()
        return [
            {"id": r.memory.id.hex(), "content": r.memory.content, "score": r.score}
            for r in results
        ]

    @app.get("/api/v1/predictions/calendar")
    def calendar_predictions():
        notifications = state.engine.check_calendar_predictions()
        formatted: list[dict[str, Any]] = []
        for item in notifications:
            surfaced: list[dict[str, Any]] = []
            for row in item.get("surfaced_memories", []):
                memory = row.get("memory")
                memory_id = getattr(memory, "id", None)
                if isinstance(memory_id, bytes):
                    memory_id = memory_id.hex()
                elif memory_id is not None:
                    memory_id = str(memory_id)
                surfaced.append(
                    {
                        "id": memory_id,
                        "content": getattr(memory, "content", ""),
                        "score": float(row.get("score", 0.0)),
                        "query": row.get("query", ""),
                        "reason": row.get("reason", ""),
                    }
                )
            formatted.append(
                {
                    "event": item.get("event", {}),
                    "count": int(item.get("count", len(surfaced))),
                    "surfaced_memories": surfaced,
                }
            )
        return formatted

    @app.post("/api/v1/consolidate")
    def consolidate(payload: Optional[dict[str, Any]] = None):
        payload = payload or {}
        phases = payload.get("phases")
        if phases is not None and not isinstance(phases, list):
            raise HTTPException(status_code=400, detail="phases must be a list")
        report = state.engine.consolidate(force=bool(payload.get("force", False)), phases=phases)
        return {
            "entities_extracted": report.entities_extracted,
            "relationships_found": report.relationships_found,
            "clusters_formed": report.clusters_formed,
            "insights_generated": report.insights_generated,
            "cross_domain_insights": int(getattr(report, "cross_domain_insights", 0)),
            "memories_promoted": report.memories_promoted,
            "memories_decayed": report.memories_decayed,
            "memories_archived": report.memories_archived,
            "duration_seconds": report.duration_seconds,
        }

    @app.get("/api/v1/insights")
    def insights(limit: int = 10):
        return [
            {"id": m.id.hex(), "content": m.content, "created_at": m.created_at}
            for m in state.engine.get_insights(limit=limit)
        ]

    @app.get("/api/v1/stats")
    def stats():
        return state.engine.stats()

    @app.get("/api/v1/search/hnsw/status")
    def hnsw_status():
        return state.engine.hnsw_status()

    @app.get("/api/v1/network/device")
    def network_device():
        return state.engine.device_identity()

    @app.get("/api/v1/network/sync/status")
    def network_sync_status():
        return state.engine.sync_status()

    @app.post("/api/v1/network/sync/bundle")
    def network_sync_bundle(payload: Optional[dict[str, Any]] = None):
        payload = payload or {}
        since_checkpoint = payload.get("since_checkpoint")
        if since_checkpoint is not None and not isinstance(since_checkpoint, str):
            raise HTTPException(status_code=400, detail="since_checkpoint must be a string")
        return state.engine.create_delta_bundle(since_checkpoint=since_checkpoint)

    @app.post("/api/v1/network/sync/apply")
    def network_sync_apply(payload: dict[str, Any]):
        if not isinstance(payload, dict):
            raise HTTPException(status_code=400, detail="payload must be a JSON object")
        try:
            return state.engine.apply_delta_bundle(payload)
        except Exception as exc:
            raise HTTPException(status_code=400, detail=str(exc))

    @app.post("/api/v1/network/sync/peer")
    def network_sync_peer(payload: dict[str, Any]):
        host = str(payload.get("host", "")).strip()
        if not host:
            raise HTTPException(status_code=400, detail="host is required")
        port = int(payload.get("port", 9473))
        since_checkpoint = payload.get("since_checkpoint")
        if since_checkpoint is not None and not isinstance(since_checkpoint, str):
            raise HTTPException(status_code=400, detail="since_checkpoint must be a string")
        return state.engine.sync_with_peer(host=host, port=port, since_checkpoint=since_checkpoint)

    @app.post("/api/v1/admin/users", dependencies=[Depends(_auth(Permission.MANAGE))])
    def admin_create_user(payload: dict[str, Any]):
        username = str(payload.get("username", "")).strip()
        if not username:
            raise HTTPException(status_code=400, detail="username is required")
        role = str(payload.get("role", "reader"))
        created_by = str(payload.get("created_by", "system"))
        try:
            return state.engine.create_user(username=username, role=role, created_by=created_by)
        except Exception as exc:
            raise HTTPException(status_code=400, detail=str(exc))

    @app.get("/api/v1/admin/users", dependencies=[Depends(_auth(Permission.MANAGE))])
    def admin_list_users():
        return state.engine.list_users()

    @app.post("/api/v1/admin/users/{username}/revoke", dependencies=[Depends(_auth(Permission.MANAGE))])
    def admin_revoke_user(username: str):
        return {"ok": state.engine.revoke_user(username)}

    @app.post("/api/v1/admin/device-token", dependencies=[Depends(_auth(Permission.MANAGE))])
    def admin_create_device_token(payload: dict[str, Any]):
        device_name = str(payload.get("device_name", "")).strip()
        if not device_name:
            raise HTTPException(status_code=400, detail="device_name is required")
        perms = payload.get("permissions", [Permission.READ])
        if isinstance(perms, str):
            perms = [perms]
        if not isinstance(perms, list):
            raise HTTPException(status_code=400, detail="permissions must be a list of strings")
        token = state.engine.create_device_token(device_name=device_name, permissions=set(str(p) for p in perms))
        return {"device_name": device_name, "token": token}

    @app.get("/api/v1/audit/log", dependencies=[Depends(_auth(Permission.ADMIN))])
    def audit_log(user: Optional[str] = None, action: Optional[str] = None, since: Optional[int] = None, limit: int = 100):
        return state.engine.query_audit(user=user, action=action, since=since, limit=limit)

    @app.get("/api/v1/entities")
    def entities(entity_type: Optional[str] = None, limit: int = 50):
        ents = state.engine.get_entities(entity_type=entity_type, limit=limit)
        return [
            {
                "id": e.id.hex() if e.id else None,
                "name": e.name,
                "entity_type": e.entity_type,
                "mention_count": e.mention_count,
                "importance": e.importance,
            }
            for e in ents
        ]

    @app.get("/api/v1/entities/{entity_id}/relationships")
    def entity_relationships(entity_id: str):
        try:
            entity = state.engine._nv.get_entity(bytes.fromhex(entity_id))
        except Exception as exc:
            logger.debug("Entity relationship lookup failed for %s: %s", entity_id, exc)
            raise HTTPException(status_code=404, detail="entity not found")
        rels = state.engine.get_relationships(entity.name)
        return [
            {
                "id": r.id.hex() if r.id else None,
                "relation_type": r.relation_type,
                "source_entity_id": r.source_entity_id.hex(),
                "target_entity_id": r.target_entity_id.hex(),
                "strength": r.strength,
                "evidence_count": r.evidence_count,
            }
            for r in rels
        ]

    @app.get("/api/v1/entities/{entity_id}/hierarchy")
    def entity_hierarchy(entity_id: str):
        try:
            return state.engine.get_entity_hierarchy(entity_id)
        except Exception as exc:
            raise HTTPException(status_code=404, detail=str(exc))

    @app.get("/api/v1/concepts/tree")
    def concept_tree(limit: int = 200):
        return state.engine.get_concept_tree(limit=limit)

    @app.get("/api/v1/relationships")
    def relationships(entity_name: str):
        rels = state.engine.get_relationships(entity_name)
        return [
            {
                "id": r.id.hex() if r.id else None,
                "relation_type": r.relation_type,
                "source_entity_id": r.source_entity_id.hex(),
                "target_entity_id": r.target_entity_id.hex(),
                "strength": r.strength,
                "evidence_count": r.evidence_count,
            }
            for r in rels
        ]

    @app.get("/api/v1/entities/{entity_name}/graph")
    def entity_graph(entity_name: str, depth: int = 2):
        graph = state.engine.get_entity_graph(entity_name, depth=depth)
        if graph is None:
            raise HTTPException(status_code=404, detail="entity not found")
        return {
            "root": graph.root_entity.name,
            "depth": graph.depth,
            "nodes": [n.name for n in graph.nodes],
            "relationships": [
                {
                    "relation_type": r.relation_type,
                    "source_entity_id": r.source_entity_id.hex(),
                    "target_entity_id": r.target_entity_id.hex(),
                    "strength": r.strength,
                }
                for r in graph.relationships
            ],
        }

    @app.get("/api/v1/entities/id/{entity_id}/graph")
    def entity_graph_by_id(entity_id: str, depth: int = 2):
        try:
            entity_bytes = bytes.fromhex(entity_id)
            entity = state.engine._nv.get_entity(entity_bytes)
        except Exception as exc:
            logger.debug("Entity graph lookup failed for id %s: %s", entity_id, exc)
            raise HTTPException(status_code=404, detail="entity not found")
        return entity_graph(entity.name, depth=depth)

    @app.get("/api/v1/review")
    def review(limit: int = 20):
        return [
            {"id": m.id.hex(), "content": m.content, "importance": m.importance}
            for m in state.engine.review_due(limit=limit)
        ]

    @app.post("/api/v1/reinforce/{memory_id}")
    def reinforce(memory_id: str):
        state.engine.reinforce(memory_id)
        return {"ok": True, "memory_id": memory_id}

    @app.post("/api/v1/decay/run")
    def decay_run():
        return state.engine.run_decay()

    @app.get("/api/v1/events")
    def events(limit: int = 50, event_type: Optional[str] = None):
        return state.engine.list_chronos_events(limit=limit, event_type=event_type)

    @app.get("/api/v1/plugins")
    def plugins():
        return state.engine.list_chronos_plugins()

    @app.get("/api/v1/plugins/local")
    def local_plugins():
        return state.engine.list_plugins()

    @app.get("/api/v1/plugins/{plugin_id}")
    def plugin_status(plugin_id: str):
        payload = state.engine.get_chronos_plugin_status(plugin_id)
        if payload.get("error"):
            raise HTTPException(status_code=404, detail=str(payload["error"]))
        return payload

    @app.post("/api/v1/plugins/{plugin_id}/activate")
    def plugin_activate(plugin_id: str, payload: Optional[dict[str, Any]] = None):
        payload = payload or {}
        config = payload.get("config", {})
        if config is not None and not isinstance(config, dict):
            raise HTTPException(status_code=400, detail="config must be an object")
        ok = state.engine.activate_chronos_plugin(plugin_id, config=config or {})
        if not ok:
            raise HTTPException(status_code=400, detail=f"failed to activate plugin '{plugin_id}'")
        return {"ok": True, "plugin_id": plugin_id}

    @app.post("/api/v1/plugins/{plugin_id}/deactivate")
    def plugin_deactivate(plugin_id: str):
        ok = state.engine.deactivate_chronos_plugin(plugin_id)
        if not ok:
            raise HTTPException(status_code=400, detail=f"failed to deactivate plugin '{plugin_id}'")
        return {"ok": True, "plugin_id": plugin_id}

    @app.post("/api/v1/plugins/{plugin_id}/enable")
    def local_plugin_enable(plugin_id: str):
        ok = state.engine.enable_plugin(plugin_id)
        if not ok:
            raise HTTPException(status_code=400, detail=f"failed to enable plugin '{plugin_id}'")
        return {"ok": True, "plugin_id": plugin_id}

    @app.post("/api/v1/plugins/{plugin_id}/disable")
    def local_plugin_disable(plugin_id: str):
        ok = state.engine.disable_plugin(plugin_id)
        if not ok:
            raise HTTPException(status_code=400, detail=f"failed to disable plugin '{plugin_id}'")
        return {"ok": True, "plugin_id": plugin_id}

    @app.post("/api/v1/chronos/graph/rebuild")
    def chronos_graph_rebuild():
        return {"ok": state.engine.rebuild_chronos_graph()}

    @app.get("/api/v1/chronos/graph/neighbors")
    def chronos_graph_neighbors(entity_name: str):
        return {
            "entity_name": entity_name,
            "neighbors": state.engine.get_chronos_graph_neighbors(entity_name),
        }

    @app.get("/api/v1/chronos/graph/path")
    def chronos_graph_path(source: str, target: str):
        return {
            "source": source,
            "target": target,
            "path": state.engine.get_chronos_graph_path(source, target),
        }

    @app.get("/api/v1/personas")
    def personas():
        return {
            "active_persona": state.engine.get_active_persona(),
            "personas": state.engine.list_personas(),
        }

    @app.post("/api/v1/personas")
    def create_persona(payload: dict[str, Any]):
        name = str(payload.get("name", "")).strip()
        if not name:
            raise HTTPException(status_code=400, detail="name is required")
        try:
            persona = state.engine.create_persona(
                name=name,
                description=str(payload.get("description", "")),
                fork_from=payload.get("fork_from"),
                make_active=bool(payload.get("make_active", False)),
            )
        except Exception as exc:
            raise HTTPException(status_code=400, detail=str(exc))
        return persona

    @app.put("/api/v1/personas/active")
    def switch_persona(payload: dict[str, Any]):
        name = str(payload.get("name", "")).strip()
        if not name:
            raise HTTPException(status_code=400, detail="name is required")
        if not state.engine.use_persona(name):
            raise HTTPException(status_code=400, detail=f"failed to switch to persona '{name}'")
        return {"active_persona": state.engine.get_active_persona()}

    @app.delete("/api/v1/personas/{name}")
    def delete_persona(name: str, force: bool = False):
        if not state.engine.delete_persona(name, force=force):
            raise HTTPException(status_code=400, detail=f"failed to delete persona '{name}'")
        return {"ok": True, "name": name}

    @app.post("/api/v1/merge")
    def merge(payload: dict[str, Any]):
        source_persona = str(payload.get("source_persona", "")).strip()
        if not source_persona:
            raise HTTPException(status_code=400, detail="source_persona is required")
        result = state.engine.merge_persona(
            source_persona=source_persona,
            strategy=str(payload.get("strategy", "semantic")),
            threshold=float(payload.get("threshold", 0.95)),
            dry_run=bool(payload.get("dry_run", False)),
        )
        if result.get("error"):
            raise HTTPException(status_code=400, detail=str(result["error"]))
        return result

    @app.get("/api/v1/checkpoints/diff")
    def checkpoint_diff(ref1: str, ref2: str, limit: int = 50):
        return state.engine.checkpoint_diff(ref1=ref1, ref2=ref2, limit=limit)

    @app.post("/api/v1/milestones")
    def create_milestone(payload: dict[str, Any]):
        name = str(payload.get("name", "")).strip()
        if not name:
            raise HTTPException(status_code=400, detail="name is required")
        try:
            return state.engine.create_milestone(
                name=name,
                ref=str(payload.get("ref", "HEAD")),
                message=str(payload.get("message", "")),
            )
        except Exception as exc:
            raise HTTPException(status_code=400, detail=str(exc))

    @app.get("/api/v1/milestones")
    def milestones(limit: int = 20):
        return state.engine.list_milestones(limit=limit)

    @app.post("/api/v1/revert")
    def revert(payload: dict[str, Any]):
        ref = str(payload.get("ref", "HEAD"))
        hard = bool(payload.get("hard", False))
        result = state.engine.revert(ref=ref, hard=hard)
        if result.get("error"):
            raise HTTPException(status_code=400, detail=str(result["error"]))
        return result

    @app.post("/api/v1/sync/push")
    def sync_push(payload: dict[str, Any]):
        peer_name = str(payload.get("peer_name", "")).strip()
        output_path = str(payload.get("output_path", "")).strip()
        if not peer_name:
            raise HTTPException(status_code=400, detail="peer_name is required")
        if not output_path:
            raise HTTPException(status_code=400, detail="output_path is required")
        return state.engine.sync_push(peer_name=peer_name, output_path=output_path)

    @app.post("/api/v1/sync/pull")
    def sync_pull(payload: dict[str, Any]):
        bundle_path = str(payload.get("bundle_path", "")).strip()
        if not bundle_path:
            raise HTTPException(status_code=400, detail="bundle_path is required")
        strategy = str(payload.get("strategy", "newer_wins"))
        return state.engine.sync_pull(bundle_path=bundle_path, strategy=strategy)

    @app.post("/api/v1/keys/generate")
    def key_generate(payload: Optional[dict[str, Any]] = None):
        payload = payload or {}
        name = str(payload.get("name", "default"))
        return state.engine.generate_key(name=name)

    @app.get("/api/v1/verify")
    def verify():
        return state.engine.verify_integrity()

    @app.get("/api/v1/sensors/status")
    def sensors_status():
        return state.engine.sensor_status()

    @app.get("/api/v1/pipeline/stats")
    def pipeline_stats():
        return state.engine.pipeline_stats()

    @app.get("/api/v1/status")
    def status():
        s = state.engine.status
        return {
            "daemon_running": s.daemon_running,
            "daemon_pid": s.daemon_pid,
            "tier_counts": s.tier_counts,
            "entity_count": s.entity_count,
            "relationship_count": s.relationship_count,
            "insight_count": s.insight_count,
            "db_size_bytes": s.db_size_bytes,
            "chronos_version": s.chronos_version,
        }

    @app.get("/api/v1/audit")
    def audit(since: Optional[int] = None):
        return state.engine.audit(since=since, format="dict")

    @app.post("/api/v1/checkpoint")
    def checkpoint(payload: dict[str, Any]):
        message = str(payload.get("message", "api checkpoint")).strip()
        cp = state.engine.checkpoint(message)
        return {"id": cp.id.hex(), "message": cp.message, "created_at": cp.created_at}

    return app


def create_mobile_app(vault_name: str = "default", config: Optional[dict[str, Any]] = None):
    """Create FastAPI mobile companion app for a vault."""
    from neurovault.mobile_api import create_mobile_api

    engine = NeurovaultEngine.open(vault_name)
    return create_mobile_api(engine, config=config or {})
