"""FastAPI service wrapper for KIESSCLAW runtime."""

from __future__ import annotations

import os
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any

import yaml
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import AnyHttpUrl, BaseModel, EmailStr, Field

from kiessclaw import __version__
from kiessclaw.codegen.scaffold import build_usecase_manifest
from kiessclaw.pipeline import LeadGenPipeline
from kiessclaw.prompting.prompt_factory import lint_prompt, render_prompt
from kiessclaw.providers import active_provider, list_providers
from kiessclaw.runtime import build_agent_registry, load_config, workspace_memory
from kiessclaw.scheduler import KiessScheduler
from kiessclaw.usecases.registry import get_usecase, list_usecases, usecase_to_dict
from kiessclaw.workflow.runner import WorkflowRunner


@dataclass
class RuntimeContext:
    """Shared runtime state for API requests."""

    config: dict[str, Any]
    registry: dict[str, Any]
    memory: Any
    pipeline: LeadGenPipeline
    scheduler: KiessScheduler


def _provider_from_config_file(config_path: str) -> str:
    """Read provider from config file without full runtime interpolation."""
    with open(config_path, encoding="utf-8") as handle:
        raw = yaml.safe_load(handle) or {}
    if not isinstance(raw, dict):
        return "openai"
    return str(raw.get("llm", {}).get("provider", "openai")).strip().lower()


def _build_context() -> RuntimeContext:
    """Build shared API runtime context and fail fast on invalid config."""
    config_path = os.getenv("KIESSCLAW_CONFIG", "config/config.yaml")
    provider = _provider_from_config_file(config_path)
    if provider == "openai" and not os.getenv("OPENAI_API_KEY", "").strip():
        print(
            "❌ OPENAI_API_KEY is required when llm.provider=openai.\n"
            "Set OPENAI_API_KEY in your environment or switch llm.provider."
        )
        raise SystemExit(1)

    config = load_config(config_path)
    registry = build_agent_registry(config)
    memory = workspace_memory(config)
    pipeline = LeadGenPipeline(config=config, registry=registry)
    scheduler = KiessScheduler(config=config, registry=registry)
    return RuntimeContext(config=config, registry=registry, memory=memory, pipeline=pipeline, scheduler=scheduler)


CONTEXT = _build_context()
STARTED_AT = time.time()

app = FastAPI(
    title="KIESSCLAW API",
    version=__version__,
    description="Autonomous SEO + SDR orchestration service",
)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class AuditRequest(BaseModel):
    """Request body for audit endpoint."""

    url: AnyHttpUrl
    auto_enroll: bool = False


class ContactCreateRequest(BaseModel):
    """Request body for contact create endpoint."""

    email: EmailStr
    first_name: str = ""
    last_name: str = ""
    title: str | None = None
    company: str | None = None
    domain: str | None = None


class EnrollRequest(BaseModel):
    """Request body for sequence enrollment endpoint."""

    contact_id: str


class SequenceCreateRequest(BaseModel):
    """Request body for sequence create endpoint."""

    name: str = Field(min_length=1)
    sender_email: EmailStr
    first_step_subject: str = "Quick idea for {{company}}"
    first_step_body: str = "Hi {{first_name}},\n\nCan I share a quick suggestion for {{company}}?"
    activate: bool = True


class ReplyRequest(BaseModel):
    """Request body for reply ingestion endpoint."""

    contact_id: str
    text: str = Field(min_length=1)


class WaitlistRequest(BaseModel):
    """Request body for waitlist sign-up endpoint."""

    name: str = Field(min_length=1)
    email: EmailStr
    company: str = ""
    role: str = ""
    team_size: str = ""
    goals: str = ""


class PromptRenderRequest(BaseModel):
    """Request body for prompt render endpoint."""

    usecase_id: str
    template: str = Field(pattern="^(system|task|guardrails|checklist)$")
    inputs: dict[str, Any]
    style: str = "strict"
    deterministic: bool = False


class ScaffoldUsecaseRequest(BaseModel):
    """Request body for scaffold usecase manifest endpoint."""

    usecase_id: str
    inputs: dict[str, Any] = Field(default_factory=dict)


class WorkflowRunRequest(BaseModel):
    """Request body for workflow execution endpoint."""

    usecase_id: str
    inputs: dict[str, Any] = Field(default_factory=dict)
    dry_run: bool = True
    auto_enroll: bool = False
    provider: str | None = None


class ScheduleCreateRequest(BaseModel):
    """Request body for schedule create endpoint."""

    usecase_id: str
    cron: str
    inputs: dict[str, Any] = Field(default_factory=dict)
    dry_run: bool = True
    job_id: str | None = None


def _agent_error(message: str, exc: Exception) -> JSONResponse:
    """Return normalized API error payload for agent/runtime failures."""
    return JSONResponse(status_code=502, content={"error": f"{message}: {exc}"})


@app.exception_handler(RequestValidationError)
def validation_exception_handler(_: Request, exc: RequestValidationError) -> JSONResponse:
    """Return consistent 422 payload for all request body/path/query validation errors."""
    return JSONResponse(status_code=422, content={"error": "Validation failed", "detail": exc.errors()})


@app.exception_handler(HTTPException)
def http_exception_handler(_: Request, exc: HTTPException) -> JSONResponse:
    """Normalize HTTP errors to consistent JSON payload shape."""
    detail = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
    return JSONResponse(status_code=exc.status_code, content={"error": detail})


@app.get("/version")
def version() -> dict[str, Any]:
    """Return release/version metadata and process uptime."""
    return {
        "version": __version__,
        "agents_loaded": len(CONTEXT.registry),
        "uptime_seconds": int(max(0, time.time() - STARTED_AT)),
    }


@app.get("/health")
def health() -> dict[str, Any]:
    """Return service health and high-level runtime state."""
    memory = CONTEXT.memory
    contacts = memory.load_data("contacts")
    sequences = memory.load_data("sequences")
    enrollments = memory.load_data("enrollments")

    agents: dict[str, str] = {}
    for codename, agent in CONTEXT.registry.items():
        agents[codename] = agent.report_status().get("status", "unknown")

    return {
        "status": "ok",
        "agents": agents,
        "version": __version__,
        "contacts": len(contacts),
        "sequences": len(sequences),
        "enrollments": len(enrollments),
    }


@app.get("/usecases")
def usecases() -> list[dict[str, Any]]:
    """Return all built-in use case specs."""
    return [usecase_to_dict(spec) for spec in list_usecases()]


@app.get("/usecases/{usecase_id}")
def usecase(usecase_id: str) -> dict[str, Any]:
    """Return one built-in use case spec by ID."""
    try:
        spec = get_usecase(usecase_id)
    except KeyError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    return usecase_to_dict(spec)


@app.post("/providers/health")
def providers_health() -> dict[str, Any]:
    """Return provider registration and active provider health by kind."""
    payload: dict[str, Any] = {"available": list_providers(), "kinds": {}}
    for kind in ("email_finder", "enrichment", "crm", "inbox"):
        provider = active_provider(kind=kind, config=CONTEXT.config)
        payload["kinds"][kind] = {
            "provider": provider.name if provider else None,
            "healthy": provider.health_check() if provider else False,
        }
    return payload


@app.post("/prompt/render")
def prompt_render(payload: PromptRenderRequest) -> dict[str, Any]:
    """Render one prompt and return lint results."""
    try:
        text = render_prompt(
            usecase_id=payload.usecase_id,
            template_name=payload.template,
            inputs=payload.inputs,
            style=payload.style,
            deterministic=payload.deterministic,
        )
    except KeyError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc

    return {
        "usecase_id": payload.usecase_id,
        "template": payload.template,
        "prompt": text,
        "lint": lint_prompt(text),
    }


@app.post("/scaffold/usecase")
def scaffold_usecase_manifest(payload: ScaffoldUsecaseRequest) -> dict[str, Any]:
    """Return scaffold manifest preview for a use case without writing files."""
    try:
        manifest = build_usecase_manifest(payload.usecase_id, payload.inputs)
    except KeyError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    return {"usecase_id": payload.usecase_id, "files": manifest}


@app.post("/workflow/run")
def workflow_run(payload: WorkflowRunRequest) -> dict[str, Any]:
    """Execute workflow use case run and return normalized result."""
    runner = WorkflowRunner(config=CONTEXT.config, registry=CONTEXT.registry)
    result = runner.run(
        usecase_id=payload.usecase_id,
        inputs=payload.inputs,
        dry_run=payload.dry_run,
        auto_enroll=payload.auto_enroll,
        provider_name=payload.provider,
    )
    if any(item.lower().startswith("unknown use case") for item in result.errors):
        raise HTTPException(status_code=422, detail=result.errors[0])
    return result.to_dict()


@app.get("/schedule")
def schedule_list() -> list[dict[str, Any]]:
    """Return all persisted scheduled workflow jobs."""
    return CONTEXT.scheduler.list_jobs()


@app.post("/schedule")
def schedule_add(payload: ScheduleCreateRequest) -> dict[str, Any]:
    """Create one persisted schedule job."""
    try:
        job_id = CONTEXT.scheduler.add_job(
            usecase_id=payload.usecase_id,
            cron=payload.cron,
            inputs=payload.inputs,
            dry_run=payload.dry_run,
            job_id=payload.job_id,
        )
    except KeyError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    return {"job_id": job_id}


@app.delete("/schedule/{job_id}")
def schedule_remove(job_id: str) -> dict[str, Any]:
    """Delete one scheduled workflow job."""
    if not CONTEXT.scheduler.remove_job(job_id):
        raise HTTPException(status_code=404, detail=f"Unknown job_id: {job_id}")
    return {"ok": True}


@app.post("/schedule/{job_id}/run")
def schedule_run(job_id: str) -> dict[str, Any]:
    """Run one scheduled workflow job immediately."""
    try:
        result = CONTEXT.scheduler.run_now(job_id)
    except ValueError as exc:
        raise HTTPException(status_code=404, detail=str(exc)) from exc
    return result.to_dict()


@app.post("/audit")
def audit(payload: AuditRequest) -> Any:
    """Run SEO audit and optional autonomous lead-generation path."""
    try:
        result = CONTEXT.pipeline.run_audit(
            url=str(payload.url),
            auto_enroll=payload.auto_enroll,
        )
    except Exception as exc:  # noqa: BLE001
        return _agent_error("Audit failed", exc)

    audit_data = result["audit"]
    lead_data = result["pipeline"]
    issues: list[str] = []

    if audit_data.get("missing_title_pages", 0) > 0:
        issues.append(f"Missing title tags: {audit_data['missing_title_pages']}")
    if audit_data.get("missing_meta_pages", 0) > 0:
        issues.append(f"Missing meta descriptions: {audit_data['missing_meta_pages']}")
    if len(audit_data.get("broken_links", [])) > 0:
        issues.append(f"Broken links: {len(audit_data['broken_links'])}")
    if not audit_data.get("robots_ok", True):
        issues.append("robots.txt not reachable")
    if not audit_data.get("sitemap_ok", True):
        issues.append("sitemap.xml not reachable")

    return {
        "domain": audit_data.get("domain"),
        "seo_score": audit_data.get("technical_score", 0),
        "issues": issues,
        "leads_generated": bool(lead_data.get("enrolled")),
    }


@app.post("/contacts")
def create_contact(payload: ContactCreateRequest) -> Any:
    """Create a contact and score ICP fit."""
    memory = CONTEXT.memory
    kpro = CONTEXT.registry["KPRO"]

    contacts = memory.load_data("contacts")
    email = payload.email.lower()
    existing = next((item for item in contacts if item.get("email") == email), None)
    if existing:
        score = int(existing.get("icp_score", 0))
        min_score = int(kpro.prospect_skill.min_icp_score)  # type: ignore[attr-defined]
        return {
            "contact_id": existing["id"],
            "icp_score": score,
            "qualified": score >= min_score,
        }

    domain = (payload.domain or email.split("@")[1]).lower()
    accounts = memory.load_data("accounts")
    account = next((item for item in accounts if item.get("domain") == domain), None)
    if not account:
        from kiessclaw.models.account import Account

        account_obj = Account(domain=domain, name=payload.company or domain)
        account = account_obj.to_dict()
        accounts.append(account)
        memory.save_data("accounts", accounts)

    from kiessclaw.models.contact import Contact

    contact = Contact(
        account_id=account["id"],
        email=email,
        first_name=payload.first_name,
        last_name=payload.last_name,
        title=payload.title,
    )
    try:
        score_result = kpro.prospect_skill.score_icp_fit(  # type: ignore[attr-defined]
            {
                "title": payload.title,
                "industry": account.get("industry"),
                "employee_count": account.get("employee_count"),
                "email": email,
            }
        )
    except Exception as exc:  # noqa: BLE001
        return _agent_error("Contact scoring failed", exc)

    contact.icp_score = int(score_result["score"])
    contact.status = "qualified" if score_result["qualified"] else "new"

    contacts.append(contact.to_dict())
    memory.save_data("contacts", contacts)

    return {
        "contact_id": contact.id,
        "icp_score": contact.icp_score,
        "qualified": bool(score_result["qualified"]),
    }


@app.get("/contacts")
def list_contacts() -> list[dict[str, Any]]:
    """Return all contacts with core scoring and status fields."""
    contacts = CONTEXT.memory.load_data("contacts")
    return [
        {
            "id": item.get("id"),
            "email": item.get("email"),
            "first_name": item.get("first_name"),
            "last_name": item.get("last_name"),
            "title": item.get("title"),
            "icp_score": item.get("icp_score", 0),
            "status": item.get("status", "new"),
        }
        for item in contacts
    ]


@app.get("/sequences")
def list_sequences() -> list[dict[str, Any]]:
    """Return sequence list with summary fields for dashboard views."""
    sequences = CONTEXT.memory.load_data("sequences")
    return [
        {
            "id": item.get("id"),
            "name": item.get("name"),
            "status": item.get("status", "draft"),
            "total_enrolled": int(item.get("total_enrolled", 0)),
            "active_enrolled": int(item.get("active_enrolled", 0)),
            "steps_count": len(item.get("steps", [])),
            "sender_email": item.get("sender_email"),
            "created_at": item.get("created_at"),
        }
        for item in sequences
    ]


@app.post("/sequences")
def create_sequence(payload: SequenceCreateRequest) -> Any:
    """Create one sequence with a default first step for rapid dashboard onboarding."""
    memory = CONTEXT.memory
    kseq = CONTEXT.registry["KSEQ"]
    sequences = memory.load_data("sequences")

    existing = next(
        (
            item
            for item in sequences
            if item.get("name", "").strip().lower() == payload.name.strip().lower()
        ),
        None,
    )
    if existing:
        return {
            "id": existing["id"],
            "name": existing.get("name"),
            "status": existing.get("status", "draft"),
            "steps_count": len(existing.get("steps", [])),
        }

    try:
        sequence = kseq.sequence_skill.create_sequence(  # type: ignore[attr-defined]
            name=payload.name.strip(),
            sender_email=payload.sender_email.lower(),
            steps=[
                {
                    "channel": "email",
                    "delay_days": 0,
                    "subject": payload.first_step_subject.strip(),
                    "body": payload.first_step_body.strip(),
                }
            ],
        )
    except Exception as exc:  # noqa: BLE001
        return _agent_error("Sequence creation failed", exc)

    if payload.activate:
        sequence["status"] = "active"
        sequence["started_at"] = datetime.now(timezone.utc).isoformat()

    sequences.append(sequence)
    memory.save_data("sequences", sequences)
    return {
        "id": sequence["id"],
        "name": sequence["name"],
        "status": sequence["status"],
        "steps_count": len(sequence.get("steps", [])),
    }


@app.post("/sequences/{sequence_id}/enroll")
def enroll_contact(sequence_id: str, payload: EnrollRequest) -> Any:
    """Enroll one contact into a specific sequence."""
    memory = CONTEXT.memory
    kseq = CONTEXT.registry["KSEQ"]

    contacts = memory.load_data("contacts")
    if not any(item.get("id") == payload.contact_id for item in contacts):
        raise HTTPException(status_code=404, detail=f"Contact not found: {payload.contact_id}")

    sequences = memory.load_data("sequences")
    sequence = next((item for item in sequences if item.get("id") == sequence_id), None)
    if not sequence:
        raise HTTPException(status_code=404, detail=f"Sequence not found: {sequence_id}")
    if sequence.get("status") != "active":
        raise HTTPException(status_code=400, detail=f"Sequence must be active. Current: {sequence.get('status')}")

    enrollments = memory.load_data("enrollments")
    existing = next(
        (
            item
            for item in enrollments
            if item.get("contact_id") == payload.contact_id
            and item.get("sequence_id") == sequence_id
            and item.get("status") in {"active", "paused", "replied", "completed"}
        ),
        None,
    )
    if existing:
        return {
            "enrollment_id": existing["id"],
            "next_send_at": existing.get("next_send_at"),
        }

    try:
        enrollment = kseq.sequence_skill.enroll_contact(  # type: ignore[attr-defined]
            sequence_id=sequence_id,
            contact_id=payload.contact_id,
            start_immediately=True,
        )
    except Exception as exc:  # noqa: BLE001
        return _agent_error("Enrollment failed", exc)

    enrollments.append(enrollment)
    memory.save_data("enrollments", enrollments)

    sequence["total_enrolled"] = int(sequence.get("total_enrolled", 0)) + 1
    sequence["active_enrolled"] = int(sequence.get("active_enrolled", 0)) + 1
    memory.save_data("sequences", sequences)

    return {
        "enrollment_id": enrollment["id"],
        "next_send_at": enrollment.get("next_send_at"),
    }


@app.get("/pipeline")
def pipeline() -> Any:
    """Return pipeline summary with contact and enrollment stage counts."""
    memory = CONTEXT.memory
    contacts = memory.load_data("contacts")
    enrollments = memory.load_data("enrollments")
    messages = memory.load_data("messages")
    replies = memory.load_data("replies")
    kmet = CONTEXT.registry["KMET"]
    try:
        pipeline_data = kmet.analytics_skill.calculate_pipeline(contacts)  # type: ignore[attr-defined]
    except Exception as exc:  # noqa: BLE001
        return _agent_error("Pipeline calculation failed", exc)

    enrollment_stages: dict[str, int] = {}
    for item in enrollments:
        stage = item.get("status", "unknown")
        enrollment_stages[stage] = enrollment_stages.get(stage, 0) + 1

    contacts_by_id = {item.get("id"): item for item in contacts}
    message_counts: dict[str, int] = {}
    latest_message_at: dict[str, str] = {}
    for item in messages:
        contact_id = item.get("contact_id")
        if not contact_id:
            continue
        message_counts[contact_id] = message_counts.get(contact_id, 0) + 1
        sent_at = str(item.get("sent_at") or "")
        if sent_at and sent_at > latest_message_at.get(contact_id, ""):
            latest_message_at[contact_id] = sent_at

    latest_reply_at: dict[str, str] = {}
    for item in replies:
        contact_id = item.get("contact_id")
        if not contact_id:
            continue
        received_at = str(item.get("received_at") or item.get("created_at") or "")
        if received_at and received_at > latest_reply_at.get(contact_id, ""):
            latest_reply_at[contact_id] = received_at

    enrollment_count_by_contact: dict[str, int] = {}
    for item in enrollments:
        contact_id = item.get("contact_id")
        if not contact_id:
            continue
        enrollment_count_by_contact[contact_id] = enrollment_count_by_contact.get(contact_id, 0) + 1

    contacted_count = sum(1 for contact_id, count in message_counts.items() if count > 0 and contact_id in contacts_by_id)
    replied_contacts = {item.get("contact_id") for item in replies if item.get("contact_id")}
    meeting_count = sum(1 for item in replies if item.get("intent") == "meeting_request")

    funnel = {
        "new": int(pipeline_data.get("status_distribution", {}).get("new", 0)),
        "qualified": int(pipeline_data.get("qualified_count", 0)),
        "enrolled": sum(enrollment_count_by_contact.values()),
        "contacted": contacted_count,
        "replied": len(replied_contacts),
        "meeting": meeting_count,
    }

    contacts_table = []
    for item in contacts:
        contact_id = item.get("id")
        last_activity = max(
            str(latest_message_at.get(contact_id, "")),
            str(latest_reply_at.get(contact_id, "")),
            str(item.get("updated_at") or item.get("created_at") or ""),
        )
        contacts_table.append(
            {
                "id": contact_id,
                "email": item.get("email"),
                "name": f"{item.get('first_name', '')} {item.get('last_name', '')}".strip(),
                "icp_score": int(item.get("icp_score", 0)),
                "status": item.get("status", "new"),
                "last_activity": last_activity or None,
            }
        )

    return {
        "contacts": pipeline_data,
        "enrollments": enrollment_stages,
        "funnel": funnel,
        "contact_table": contacts_table,
    }


@app.post("/reply")
def reply(payload: ReplyRequest) -> Any:
    """Ingest a reply and process inbox classification + sequence stop logic."""
    memory = CONTEXT.memory
    kinb = CONTEXT.registry["KINB"]
    contacts = memory.load_data("contacts")
    contact = next((item for item in contacts if item.get("id") == payload.contact_id), None)
    if not contact:
        raise HTTPException(status_code=404, detail=f"Contact not found: {payload.contact_id}")

    replies = memory.load_data("replies")
    reply_id = f"reply_{len(replies) + 1}_{int(datetime.now().timestamp())}"
    reply_record = {
        "id": reply_id,
        "sent_message_id": None,
        "contact_id": payload.contact_id,
        "raw_text": payload.text,
        "subject": "",
        "from_email": contact.get("email"),
        "intent": "unknown",
        "intent_confidence": 0.0,
        "objection_type": None,
        "sentiment": None,
        "meeting_requested": False,
        "suggested_time": None,
        "referral_name": None,
        "referral_email": None,
        "processed": False,
        "processed_at": None,
        "processed_by": None,
        "actions_taken": [],
        "next_action": None,
        "received_at": datetime.now(timezone.utc).isoformat(),
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    replies.append(reply_record)
    memory.save_data("replies", replies)

    try:
        kinb.run("process", limit=50)
    except Exception as exc:  # noqa: BLE001
        return _agent_error("Reply processing failed", exc)

    updated_replies = memory.load_data("replies")
    processed_reply = next((item for item in updated_replies if item.get("id") == reply_id), None)
    if not processed_reply:
        return JSONResponse(status_code=502, content={"error": "Reply processing failed"})

    enrollments = memory.load_data("enrollments")
    relevant = [item for item in enrollments if item.get("contact_id") == payload.contact_id]
    sequence_stopped = bool(relevant) and all(
        item.get("status") in {"replied", "completed", "bounced"} or item.get("next_send_at") is None
        for item in relevant
    )

    return {
        "intent": processed_reply.get("intent", "unknown"),
        "sequence_stopped": sequence_stopped,
    }


@app.get("/inbox")
def inbox() -> list[dict[str, Any]]:
    """Return classified replies enriched with contact identity data."""
    memory = CONTEXT.memory
    contacts = memory.load_data("contacts")
    contact_by_id = {item.get("id"): item for item in contacts}
    replies = memory.load_data("replies")
    sorted_replies = sorted(replies, key=lambda item: item.get("received_at") or "", reverse=True)

    return [
        {
            "id": item.get("id"),
            "contact_id": item.get("contact_id"),
            "contact_name": (
                f"{contact_by_id.get(item.get('contact_id'), {}).get('first_name', '')} "
                f"{contact_by_id.get(item.get('contact_id'), {}).get('last_name', '')}"
            ).strip()
            or contact_by_id.get(item.get("contact_id"), {}).get("email")
            or "Unknown contact",
            "contact_email": contact_by_id.get(item.get("contact_id"), {}).get("email"),
            "intent": item.get("intent", "unknown"),
            "raw_text": item.get("raw_text", ""),
            "received_at": item.get("received_at"),
            "processed": bool(item.get("processed", False)),
        }
        for item in sorted_replies
    ]


@app.get("/analytics")
def analytics() -> Any:
    """Return full analytics data used by operational dashboards."""
    memory = CONTEXT.memory
    kmet = CONTEXT.registry["KMET"]
    skill = kmet.analytics_skill  # type: ignore[attr-defined]

    sequences = memory.load_data("sequences")
    contacts = memory.load_data("contacts")
    enrollments = memory.load_data("enrollments")
    messages = memory.load_data("messages")
    replies = memory.load_data("replies")

    try:
        sequence_funnels = [
            {
                "sequence_id": sequence.get("id"),
                "name": sequence.get("name"),
                "funnel": skill.calculate_sequence_funnel(sequence.get("id"), enrollments, messages, replies),
            }
            for sequence in sequences
        ]

        deliverability = skill.calculate_deliverability(messages, days=30)
        pipeline_metrics = skill.calculate_pipeline(contacts)
        reply_metrics = skill.analyze_replies(replies, days=30)
    except Exception as exc:  # noqa: BLE001
        return _agent_error("Analytics calculation failed", exc)
    enrolled = len(enrollments)
    contacted = sum(1 for item in messages if item.get("sent_at"))
    delivered = sum(1 for item in messages if item.get("sent_at") and not item.get("bounced"))
    opened = sum(1 for item in messages if item.get("opened_at"))
    replied = int(reply_metrics.get("total_replies", 0))
    replied_rate = replied / enrolled if enrolled > 0 else 0
    open_rate = opened / delivered if delivered > 0 else 0

    return {
        "deliverability": deliverability,
        "replies": reply_metrics,
        "pipeline": pipeline_metrics,
        "sequences": sequence_funnels,
        "summary": {
            "contacts_total": int(pipeline_metrics.get("total_contacts", 0)),
            "qualified": int(pipeline_metrics.get("qualified_count", 0)),
            "enrolled": enrolled,
            "contacted": contacted,
            "replied": replied,
            "reply_rate": replied_rate,
            "open_rate": open_rate,
        },
    }


@app.post("/waitlist")
def waitlist_signup(payload: WaitlistRequest) -> dict[str, Any]:
    """Persist waitlist signup payload and return queue position."""
    memory = CONTEXT.memory
    waitlist = memory.load_data("waitlist")
    waitlist.append(
        {
            "name": payload.name.strip(),
            "email": payload.email.lower(),
            "company": payload.company.strip(),
            "role": payload.role.strip(),
            "team_size": payload.team_size.strip(),
            "goals": payload.goals.strip(),
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    memory.save_data("waitlist", waitlist)
    return {"ok": True, "position": len(waitlist)}


@app.get("/waitlist/count")
def waitlist_count() -> dict[str, int]:
    """Return waitlist signup count."""
    waitlist = CONTEXT.memory.load_data("waitlist")
    return {"count": len(waitlist)}
