"""Connectors router: install, configure, test, and discover tools.

Secrets are accepted on write, encrypted via the vault, and never returned.
"""

from __future__ import annotations

import uuid

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from legate.api.deps import Principal, require
from legate.api.schemas import (
    ConnectorCreate,
    ConnectorOut,
    ConnectorTestResult,
    ConnectorTypeOut,
    ConnectorUpdate,
    ToolOut,
)
from legate.audit.service import record as audit_record
from legate.connectors.base import ConnectorConfigError
from legate.connectors.mcp import McpConnector
from legate.connectors.registry import CONNECTOR_REGISTRY, get_connector
from legate.db.session import get_db
from legate.policy.rbac import Permission
from legate.repositories import connectors as connectors_repo
from legate.repositories import credentials as credentials_repo
from legate.vault import get_vault

router = APIRouter(tags=["connectors"])


async def _refresh_mcp_tools(db: Session, row, config: dict, secrets: dict) -> None:
    """Discover an MCP server's tools and cache them on the connector row."""
    try:
        tools = await McpConnector().discover(config, secrets)
        row.config_json = {**(row.config_json or {}), "mcp_tools": tools}
        row.status = "ok"
        row.status_detail = f"{len(tools)} tools discovered"
    except Exception as exc:  # discovery must never block install
        row.status = "error"
        row.status_detail = f"tool discovery failed: {exc}"
    db.add(row)


@router.get("/connector-types", response_model=list[ConnectorTypeOut])
def list_connector_types(
    _: Principal = Depends(require(Permission.VIEW)),
) -> list[ConnectorTypeOut]:
    return [
        ConnectorTypeOut(
            type=c.type,
            title=c.title,
            config_schema=c.config_schema,
            secret_schema=c.secret_schema,
            tools=[t.name for t in c.list_tools()],
        )
        for c in CONNECTOR_REGISTRY.values()
    ]


@router.get("/connectors", response_model=list[ConnectorOut])
def list_connectors(
    principal: Principal = Depends(require(Permission.VIEW)),
    db: Session = Depends(get_db),
) -> list[ConnectorOut]:
    rows = connectors_repo.list_for_workspace(db, workspace_id=principal.workspace_id)
    return [ConnectorOut.model_validate(r) for r in rows]


@router.post("/connectors", response_model=ConnectorOut, status_code=status.HTTP_201_CREATED)
async def install_connector(
    body: ConnectorCreate,
    principal: Principal = Depends(require(Permission.MANAGE_CONNECTORS)),
    db: Session = Depends(get_db),
) -> ConnectorOut:
    connector = get_connector(body.type)
    if connector is None:
        raise HTTPException(422, detail=f"unknown connector type: {body.type}")
    try:
        connector.validate_config(body.config)
        connector.validate_secrets(body.secrets)
    except ConnectorConfigError as exc:
        raise HTTPException(422, detail=str(exc)) from exc

    row = connectors_repo.create(
        db,
        workspace_id=principal.workspace_id,
        type=body.type,
        name=body.name,
        config_json=body.config,
        created_by=principal.user.id if principal.user else None,
    )
    encrypted = get_vault().encrypt(body.secrets)
    credentials_repo.upsert(
        db,
        workspace_id=principal.workspace_id,
        connector_id=row.id,
        encrypted=encrypted,
        meta={"secret_keys": sorted(body.secrets.keys())},
    )
    # MCP servers have dynamic tools; discover and cache them on the row so the
    # tool registry can offer them without a network call on every run.
    if isinstance(connector, McpConnector):
        await _refresh_mcp_tools(db, row, body.config, body.secrets)
    audit_record(
        db,
        workspace_id=principal.workspace_id,
        actor=principal.actor,
        action="connector.install",
        target=str(row.id),
        metadata={"type": body.type, "name": body.name},
    )
    db.commit()
    db.refresh(row)
    return ConnectorOut.model_validate(row)


@router.get("/connectors/{connector_id}", response_model=ConnectorOut)
def get_connector_instance(
    connector_id: uuid.UUID,
    principal: Principal = Depends(require(Permission.VIEW)),
    db: Session = Depends(get_db),
) -> ConnectorOut:
    row = connectors_repo.get(db, workspace_id=principal.workspace_id, connector_id=connector_id)
    if row is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="connector not found")
    return ConnectorOut.model_validate(row)


@router.patch("/connectors/{connector_id}", response_model=ConnectorOut)
def update_connector(
    connector_id: uuid.UUID,
    body: ConnectorUpdate,
    principal: Principal = Depends(require(Permission.MANAGE_CONNECTORS)),
    db: Session = Depends(get_db),
) -> ConnectorOut:
    row = connectors_repo.get(db, workspace_id=principal.workspace_id, connector_id=connector_id)
    if row is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="connector not found")
    connector = get_connector(row.type)
    assert connector is not None

    if body.name is not None:
        row.name = body.name
    if body.config is not None:
        try:
            connector.validate_config(body.config)
        except ConnectorConfigError as exc:
            raise HTTPException(422, detail=str(exc)) from exc
        row.config_json = body.config
    if body.secrets is not None:
        try:
            connector.validate_secrets(body.secrets)
        except ConnectorConfigError as exc:
            raise HTTPException(422, detail=str(exc)) from exc
        credentials_repo.upsert(
            db,
            workspace_id=principal.workspace_id,
            connector_id=row.id,
            encrypted=get_vault().encrypt(body.secrets),
            meta={"secret_keys": sorted(body.secrets.keys())},
        )
    row.status = "untested"
    audit_record(
        db,
        workspace_id=principal.workspace_id,
        actor=principal.actor,
        action="connector.update",
        target=str(row.id),
    )
    db.commit()
    db.refresh(row)
    return ConnectorOut.model_validate(row)


@router.delete("/connectors/{connector_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_connector(
    connector_id: uuid.UUID,
    principal: Principal = Depends(require(Permission.MANAGE_CONNECTORS)),
    db: Session = Depends(get_db),
) -> None:
    row = connectors_repo.get(db, workspace_id=principal.workspace_id, connector_id=connector_id)
    if row is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="connector not found")
    connectors_repo.delete(db, row)
    audit_record(
        db,
        workspace_id=principal.workspace_id,
        actor=principal.actor,
        action="connector.delete",
        target=str(connector_id),
    )
    db.commit()


@router.get("/connectors/{connector_id}/tools", response_model=list[ToolOut])
def list_connector_tools(
    connector_id: uuid.UUID,
    principal: Principal = Depends(require(Permission.VIEW)),
    db: Session = Depends(get_db),
) -> list[ToolOut]:
    row = connectors_repo.get(db, workspace_id=principal.workspace_id, connector_id=connector_id)
    if row is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="connector not found")
    connector = get_connector(row.type)
    if connector is None:
        return []
    return [ToolOut(**spec.as_dict()) for spec in connector.list_tools()]


@router.post("/connectors/{connector_id}/test", response_model=ConnectorTestResult)
async def test_connector(
    connector_id: uuid.UUID,
    principal: Principal = Depends(require(Permission.MANAGE_CONNECTORS)),
    db: Session = Depends(get_db),
) -> ConnectorTestResult:
    row = connectors_repo.get(db, workspace_id=principal.workspace_id, connector_id=connector_id)
    if row is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="connector not found")
    connector = get_connector(row.type)
    if connector is None:
        raise HTTPException(422, detail=f"unknown connector type: {row.type}")

    credential = credentials_repo.get_for_connector(db, connector_id=row.id)
    if credential is None:
        raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="connector has no credentials")
    secrets = get_vault().decrypt(
        credential.ciphertext, credential.wrapped_dek, credential.key_version
    )

    result = await connector.test_connection(row.config_json or {}, secrets)
    row.status = "ok" if result.ok else "error"
    row.status_detail = result.message
    # Testing an MCP server is also a good moment to refresh its cached tools.
    if isinstance(connector, McpConnector) and result.ok:
        await _refresh_mcp_tools(db, row, row.config_json or {}, secrets)
    audit_record(
        db,
        workspace_id=principal.workspace_id,
        actor=principal.actor,
        action="connector.test",
        target=str(row.id),
        metadata={"ok": result.ok},
    )
    db.commit()
    return ConnectorTestResult(ok=result.ok, message=result.message, status=row.status)
