import re
import uuid
from datetime import datetime, timezone
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from api.database import get_session
from api.flow_defs import assertion_result_to_dict, evaluate_transition, flow_result_to_dict
from api.models.agent_message import AgentMessage
from api.models.capability_catalog import CapabilityCatalog
from api.models.capability_request import CapabilityRequest
from api.models.domain import Domain
from api.models.managed_repo import ManagedRepo
from api.models.task import Task
from api.services.suggestion_relevance import bump_matching_for_capability_request
from api.schemas.capability_request import (
    CapabilityRequestAccept,
    CapabilityRequestCreate,
    CapabilityRequestDispute,
    CapabilityRequestPatch,
    CapabilityRequestRead,
    CapabilityRequestReroute,
    CapabilityRequestStatusPatch,
)
from hub_core.routers.capabilities import (
    create_capability_catalog_router,
    create_capability_request_read_router,
    create_capability_request_write_router,
)


# ---------------------------------------------------------------------------
# Write-router callbacks
# ---------------------------------------------------------------------------

async def _route_capability(
    session: AsyncSession,
    body: CapabilityRequestCreate,
) -> tuple[uuid.UUID | None, uuid.UUID | None, str | None]:
    fulfilling_domain_id, catalog_entry_id, routing_note = await _route_capability_match(
        session,
        body.capability_type,
        body.title,
        body.description or "",
    )
    return fulfilling_domain_id, catalog_entry_id, routing_note


def _build_capability_request(
    body: CapabilityRequestCreate,
    requesting_domain: Domain,
    fulfilling_domain_id: uuid.UUID | None,
    catalog_entry_id: uuid.UUID | None,
    routing_note: str | None,
) -> CapabilityRequest:
    return CapabilityRequest(
        title=body.title,
        description=body.description,
        capability_type=body.capability_type,
        priority=body.priority,
        requesting_domain_id=requesting_domain.id,
        requesting_agent=body.requesting_agent,
        requesting_workplan_id=body.requesting_workplan_id,
        blocking_task_id=body.blocking_task_id,
        fulfilling_domain_id=fulfilling_domain_id,
        catalog_entry_id=catalog_entry_id,
        routing_note=routing_note,
    )


async def _on_capability_request_persisted(
    session: AsyncSession,
    req: CapabilityRequest,
    body: CapabilityRequestCreate,
) -> None:
    await session.flush()
    await bump_matching_for_capability_request(
        session,
        request_id=req.id,
        title=body.title,
        description=body.description or "",
        capability_type=body.capability_type,
        catalog_entry_id=req.catalog_entry_id,
    )
    await _notify_on_create(session, req, body)


async def _notify_on_create(
    session: AsyncSession,
    req: CapabilityRequest,
    body: CapabilityRequestCreate,
) -> None:
    await session.flush()

    if req.fulfilling_domain_id:
        ful_domain = await session.get(Domain, req.fulfilling_domain_id)
        to_agent = ful_domain.slug if ful_domain else "broadcast"
    else:
        to_agent = "broadcast"

    _add_notification(
        session,
        from_agent="system",
        to_agent=to_agent,
        subject=f"[capability-request] {body.title}",
        body=(
            f"New capability request from **{body.requesting_agent}** "
            f"({body.requesting_domain}):\n\n"
            f"**Type:** {body.capability_type}\n"
            f"**Priority:** {body.priority}\n\n"
            f"{body.description or '(no description)'}"
        ),
    )


def _apply_accept_fields(req: CapabilityRequest, body: CapabilityRequestAccept) -> None:
    req.fulfilling_workplan_id = body.fulfilling_workplan_id


async def _notify_on_accept(
    session: AsyncSession,
    req: CapabilityRequest,
    body: CapabilityRequestAccept,
) -> None:
    _add_notification(
        session,
        from_agent=body.fulfilling_agent,
        to_agent=req.requesting_agent,
        subject=f"[capability-accepted] {req.title}",
        body=f"Your capability request **{req.title}** has been accepted by **{body.fulfilling_agent}**.",
    )


async def _on_status_change(
    session: AsyncSession,
    req: CapabilityRequest,
    body: CapabilityRequestStatusPatch,
    now: datetime,
) -> None:
    if body.status == "completed":
        if req.blocking_task_id:
            task = await session.get(Task, req.blocking_task_id)
            if task and task.status == "wait":
                task.status = "todo"
                task.blocking_reason = None

        _add_notification(
            session,
            from_agent="system",
            to_agent=req.requesting_agent,
            subject=f"[capability-completed] {req.title}",
            body=(
                f"Capability request **{req.title}** has been completed.\n\n"
                f"{body.note or ''}"
            ),
        )
    elif body.status == "ready_for_review":
        _add_notification(
            session,
            from_agent=req.fulfilling_agent or "system",
            to_agent=req.requesting_agent,
            subject=f"[capability-ready] {req.title} -- please review",
            body=(
                f"Capability **{req.title}** is ready for your review and optimization.\n\n"
                f"{body.note or ''}"
            ),
        )
    elif body.status == "rejected":
        _add_notification(
            session,
            from_agent=req.fulfilling_agent or "system",
            to_agent=req.requesting_agent,
            subject=f"[capability-rejected] {req.title}",
            body=(
                f"Capability request **{req.title}** has been rejected.\n\n"
                f"**Reason:** {body.note or '(no reason given)'}"
            ),
        )
    elif body.status == "in_progress":
        _add_notification(
            session,
            from_agent=req.fulfilling_agent or "system",
            to_agent=req.requesting_agent,
            subject=f"[capability-in-progress] {req.title}",
            body=f"Work on capability **{req.title}** is now in progress.",
        )


async def _apply_capability_patch(
    session: AsyncSession,
    req: CapabilityRequest,
    body: CapabilityRequestPatch,
) -> bool:
    corrections: list[str] = []

    if body.catalog_entry_id is not None:
        old_entry_id = req.catalog_entry_id
        entry = await session.get(CapabilityCatalog, body.catalog_entry_id)
        if entry is None:
            raise HTTPException(status_code=404, detail=f"Catalog entry '{body.catalog_entry_id}' not found")
        req.catalog_entry_id = entry.id
        req.fulfilling_domain_id = entry.domain_id
        corrections.append(
            f"catalog_entry: {old_entry_id} → {entry.id} ({entry.title}); "
            f"fulfilling_domain re-derived → {entry.domain_id}"
        )

    if body.priority is not None:
        req.priority = body.priority
        corrections.append(f"priority → {body.priority}")

    if body.blocking_task_id is not None:
        req.blocking_task_id = body.blocking_task_id
        corrections.append(f"blocking_task_id → {body.blocking_task_id}")

    if body.fulfilling_workplan_id is not None:
        req.fulfilling_workplan_id = body.fulfilling_workplan_id
        corrections.append(f"fulfilling_workplan_id → {body.fulfilling_workplan_id}")

    if not corrections:
        return False

    correction_note = "hub correction: " + "; ".join(corrections)
    req.routing_note = (req.routing_note + "\n" + correction_note) if req.routing_note else correction_note
    return True


async def _notify_on_dispute(
    session: AsyncSession,
    req: CapabilityRequest,
    body: CapabilityRequestDispute,
    now: datetime,
) -> None:
    dispute_entry = (
        f"disputed by {body.disputed_by}: {body.reason}"
        + (f" (suggested: {body.suggested_domain})" if body.suggested_domain else "")
    )
    req.routing_note = (req.routing_note + "\n" + dispute_entry) if req.routing_note else dispute_entry

    _add_notification(
        session,
        from_agent=body.disputed_by,
        to_agent="custodian",
        subject=f"[routing-disputed] {req.title}",
        body=(
            f"**{body.disputed_by}** has disputed the routing of capability request "
            f"**{req.title}**.\n\n"
            f"**Reason:** {body.reason}\n"
            + (f"**Suggested domain:** {body.suggested_domain}\n" if body.suggested_domain else "")
            + f"\nCurrently routed to: {req.fulfilling_domain_slug or 'unrouted'}"
        ),
    )
    if req.fulfilling_domain_slug:
        _add_notification(
            session,
            from_agent=body.disputed_by,
            to_agent=req.fulfilling_domain_slug,
            subject=f"[routing-disputed] {req.title}",
            body=(
                f"The capability request **{req.title}** routed to your domain has been disputed "
                f"by **{body.disputed_by}**.\n\n"
                f"**Reason:** {body.reason}\n"
                + (f"**Suggested domain:** {body.suggested_domain}" if body.suggested_domain else "")
            ),
        )


async def _notify_on_reroute(
    session: AsyncSession,
    req: CapabilityRequest,
    body: CapabilityRequestReroute,
    new_domain_slug: str,
) -> None:
    _add_notification(
        session,
        from_agent=body.rerouted_by,
        to_agent=req.requesting_agent,
        subject=f"[re-routed] {req.title}",
        body=(
            f"Capability request **{req.title}** has been re-routed to **{new_domain_slug}**.\n\n"
            f"**Note:** {body.note}"
        ),
    )
    _add_notification(
        session,
        from_agent=body.rerouted_by,
        to_agent=new_domain_slug,
        subject=f"[capability-request] {req.title}",
        body=(
            f"Capability request **{req.title}** has been re-routed to your domain.\n\n"
            f"**From:** {req.requesting_agent} ({req.requesting_domain_slug})\n"
            f"**Type:** {req.capability_type}\n"
            f"**Priority:** {req.priority}\n\n"
            f"{req.description or '(no description)'}"
        ),
    )


# ---------------------------------------------------------------------------
# Routing algorithm
# ---------------------------------------------------------------------------

async def _route_capability_match(
    session: AsyncSession,
    capability_type: str,
    title: str,
    description: str,
) -> tuple[uuid.UUID | None, uuid.UUID | None, str]:
    """Find the best-matching catalog entry for a capability request.

    Returns (domain_id, catalog_entry_id, routing_note).
    """
    q = select(CapabilityCatalog).where(
        CapabilityCatalog.capability_type == capability_type,
        CapabilityCatalog.status == "active",
    )
    entries = list((await session.execute(q)).scalars().all())

    if not entries:
        return None, None, f"no active catalog entries for type '{capability_type}' — broadcast"

    if len(entries) == 1:
        entry = entries[0]
        return entry.domain_id, entry.id, f"single match: '{entry.title}' (domain={entry.domain_id})"

    combined = f"{title} {description or ''}".lower()
    scored: list[tuple[int, CapabilityCatalog]] = []
    for entry in entries:
        keywords = [kw for kw in (entry.keywords or []) if len(kw) >= 3]
        score = sum(
            1 for kw in keywords
            if re.search(r"\b" + re.escape(kw.lower()) + r"\b", combined)
        )
        scored.append((score, entry))
    scored.sort(key=lambda item: -item[0])

    best_score, best = scored[0]
    if best_score == 0:
        return None, None, (
            f"no keyword overlap for type '{capability_type}' among "
            f"{len(entries)} entries — broadcast"
        )
    if len(scored) >= 2 and scored[1][0] == best_score:
        return None, None, (
            f"ambiguous routing: '{scored[0][1].title}' and '{scored[1][1].title}' "
            f"both scored {best_score} — broadcast"
        )

    return best.domain_id, best.id, (
        f"matched '{best.title}' (score={best_score}, "
        f"keywords matched from: {title!r})"
    )


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _add_notification(
    session: AsyncSession,
    from_agent: str,
    to_agent: str,
    subject: str,
    body: str,
) -> None:
    msg = AgentMessage(
        from_agent=from_agent,
        to_agent=to_agent,
        subject=subject,
        body=body,
    )
    session.add(msg)


def _check_transition(current: str, target: str) -> None:
    can_reach, failures, flow_result = evaluate_transition(
        "capability_request",
        current,
        target,
    )
    if not can_reach:
        raise HTTPException(
            status_code=422,
            detail={
                "message": f"Cannot transition from '{current}' to '{target}'.",
                "current_workstation": current,
                "target_workstation": target,
                "blocking_assertions": [
                    assertion_result_to_dict(item) for item in failures
                ],
                "flow_result": flow_result_to_dict(flow_result),
            },
        )


router = create_capability_catalog_router(
    get_session,
    domain_model=Domain,
    repo_model=ManagedRepo,
    catalog_model=CapabilityCatalog,
)
router.include_router(
    create_capability_request_read_router(
        get_session,
        domain_model=Domain,
        request_model=CapabilityRequest,
        request_read_schema=CapabilityRequestRead,
    )
)
router.include_router(
    create_capability_request_write_router(
        get_session,
        domain_model=Domain,
        catalog_model=CapabilityCatalog,
        request_model=CapabilityRequest,
        request_create_schema=CapabilityRequestCreate,
        request_accept_schema=CapabilityRequestAccept,
        request_patch_schema=CapabilityRequestPatch,
        request_status_patch_schema=CapabilityRequestStatusPatch,
        request_dispute_schema=CapabilityRequestDispute,
        request_reroute_schema=CapabilityRequestReroute,
        request_read_schema=CapabilityRequestRead,
        route_request=_route_capability,
        build_request=_build_capability_request,
        on_request_persisted=_on_capability_request_persisted,
        check_transition=_check_transition,
        apply_accept_fields=_apply_accept_fields,
        after_accept=_notify_on_accept,
        after_status_change=_on_status_change,
        apply_patch=_apply_capability_patch,
        after_dispute=_notify_on_dispute,
        after_reroute=_notify_on_reroute,
    )
)