from __future__ import annotations

from datetime import datetime, timezone
import json

from fastapi import Depends, FastAPI, HTTPException, Query
from sqlalchemy import func, or_, select

from services.shared.core import Role, new_id, utc_now_iso
from services.shared.db import get_session
from services.shared.event_bus import append_outbox_event, event_bus_enabled
from services.shared.models import (
    AssignRequest,
    EscalateRequest,
    HealthResponse,
    InteractionCreate,
    InteractionDrilldownFiltersOut,
    InteractionDrilldownOut,
    InteractionTimelineAppendIn,
    InteractionOut,
    ReportingInteractionFactIn,
    StatusRequest,
)
from services.shared.reporting_facts import upsert_reporting_interaction_fact
from services.shared.security import require_roles
from services.shared.sql_init import init_sql_schema
from services.shared.sql_models import Interaction, InteractionTimeline

app = FastAPI(title="interaction-service", version="1.0.0")

init_sql_schema()

_DRILLDOWN_SORT_COLUMNS = {
    "created_at": Interaction.created_at,
    "updated_at": Interaction.updated_at,
    "subject": Interaction.subject,
}
_DRILLDOWN_SORT_DIRECTIONS = {"asc", "desc"}


def _to_out(row: Interaction) -> InteractionOut:
    return InteractionOut(
        interaction_id=row.interaction_id,
        channel=row.channel,
        subject=row.subject,
        customer_id=row.customer_id,
        queue_id=row.queue_id,
        priority=row.priority,
        status=row.status,
        assigned_to=row.assigned_to,
        created_at=row.created_at,
        updated_at=row.updated_at,
    )


def _push_event(session, interaction_id: str, action: str, metadata: dict | None = None) -> None:
    session.add(
        InteractionTimeline(
            interaction_id=interaction_id,
            timestamp=utc_now_iso(),
            action=action,
            metadata_json=json.dumps(metadata or {}, ensure_ascii=False),
        )
    )


def _reporting_fact_payload(
    row: Interaction,
    *,
    source: str,
    closed_at: str | None = None,
    resolved_first_contact: bool | None = None,
) -> dict:
    return ReportingInteractionFactIn(
        interaction_id=row.interaction_id,
        channel=row.channel,  # type: ignore[arg-type]
        queue_id=row.queue_id,
        agent_id=row.assigned_to,
        status=row.status,  # type: ignore[arg-type]
        created_at=row.created_at,
        closed_at=closed_at,
        resolved_first_contact=resolved_first_contact,
        source=source,
    ).model_dump(exclude_none=True)


def _upsert_reporting_fact(
    session,
    row: Interaction,
    *,
    source: str,
    closed_at: str | None = None,
    resolved_first_contact: bool | None = None,
) -> None:
    upsert_reporting_interaction_fact(
        session,
        _reporting_fact_payload(
            row,
            source=source,
            closed_at=closed_at,
            resolved_first_contact=resolved_first_contact,
        ),
    )


def _parse_filter_timestamp(raw: str, field_name: str) -> datetime:
    normalized = raw.strip()
    if normalized.endswith("Z"):
        normalized = f"{normalized[:-1]}+00:00"
    try:
        value = datetime.fromisoformat(normalized)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=f"Invalid {field_name}") from exc
    if value.tzinfo is None:
        value = value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)


def _normalize_drilldown_sort(sort_by: str | None, sort_dir: str | None) -> tuple[str, str]:
    normalized_sort_by = (sort_by or "created_at").strip() or "created_at"
    normalized_sort_dir = (sort_dir or "desc").strip().lower() or "desc"
    if normalized_sort_by not in _DRILLDOWN_SORT_COLUMNS:
        raise HTTPException(status_code=400, detail="Invalid sort_by")
    if normalized_sort_dir not in _DRILLDOWN_SORT_DIRECTIONS:
        raise HTTPException(status_code=400, detail="Invalid sort_dir")
    return normalized_sort_by, normalized_sort_dir


def _drilldown_order_by(sort_by: str, sort_dir: str):
    column = _DRILLDOWN_SORT_COLUMNS[sort_by]
    direction = column.asc() if sort_dir == "asc" else column.desc()
    id_order = Interaction.id.asc() if sort_dir == "asc" else Interaction.id.desc()
    if sort_by == "subject":
        created_order = Interaction.created_at.asc() if sort_dir == "asc" else Interaction.created_at.desc()
        return [direction, created_order, id_order]
    return [direction, id_order]


@app.get("/health", response_model=HealthResponse)
def health() -> HealthResponse:
    return HealthResponse(status="ok", service="interaction-service")


@app.post("/interactions", response_model=InteractionOut)
def create_interaction(
    payload: InteractionCreate,
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR, Role.OPERATOR)),
) -> InteractionOut:
    session = get_session()
    try:
        now = utc_now_iso()
        row = Interaction(
            interaction_id=new_id("int"),
            channel=payload.channel,
            subject=payload.subject,
            customer_id=payload.customer_id,
            queue_id=payload.queue_id,
            priority=payload.priority,
            status="new",
            assigned_to=None,
            created_at=now,
            updated_at=now,
        )
        session.add(row)
        _push_event(session, row.interaction_id, "interaction.created", {"channel": payload.channel})
        _upsert_reporting_fact(session, row, source="interaction-service")
        if event_bus_enabled():
            append_outbox_event(
                session,
                event_type="interaction.created",
                producer_service="interaction-service",
                entity_type="interaction",
                entity_id=row.interaction_id,
                payload={
                    "event": "interaction.created",
                    "interaction_id": row.interaction_id,
                    "channel": payload.channel,
                    "customer_id": payload.customer_id,
                    "queue_id": payload.queue_id,
                    "priority": payload.priority,
                    "status": "new",
                    "created_at": now,
                },
            )
        session.commit()
        session.refresh(row)
        return _to_out(row)
    finally:
        session.close()


@app.get("/interactions/drilldown", response_model=InteractionDrilldownOut)
def drilldown_interactions(
    from_ts: str = Query(...),
    to_ts: str = Query(...),
    queue_id: str | None = None,
    channel: str | None = None,
    agent_id: str | None = None,
    status: str | None = None,
    q: str | None = None,
    sort_by: str = Query(default="created_at"),
    sort_dir: str = Query(default="desc"),
    limit: int = Query(default=25, ge=1, le=100),
    offset: int = Query(default=0, ge=0),
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR, Role.ANALYST)),
) -> InteractionDrilldownOut:
    range_from = _parse_filter_timestamp(from_ts, "from_ts")
    range_to = _parse_filter_timestamp(to_ts, "to_ts")
    if range_to <= range_from:
        raise HTTPException(status_code=400, detail="to_ts must be greater than from_ts")
    normalized_q = (q or "").strip()
    normalized_sort_by, normalized_sort_dir = _normalize_drilldown_sort(sort_by, sort_dir)

    filters = [
        Interaction.created_at >= range_from.isoformat(),
        Interaction.created_at < range_to.isoformat(),
    ]
    if queue_id:
        filters.append(Interaction.queue_id == queue_id)
    if channel:
        filters.append(Interaction.channel == channel)
    if agent_id:
        filters.append(Interaction.assigned_to == agent_id)
    if status:
        filters.append(Interaction.status == status)
    if normalized_q:
        search_pattern = f"%{normalized_q.lower()}%"
        filters.append(
            or_(
                func.lower(func.coalesce(Interaction.interaction_id, "")).like(search_pattern),
                func.lower(func.coalesce(Interaction.subject, "")).like(search_pattern),
                func.lower(func.coalesce(Interaction.assigned_to, "")).like(search_pattern),
            )
        )

    session = get_session()
    try:
        total = session.execute(select(func.count()).select_from(Interaction).where(*filters)).scalar_one()
        rows = session.execute(
            select(Interaction)
            .where(*filters)
            .order_by(*_drilldown_order_by(normalized_sort_by, normalized_sort_dir))
            .offset(offset)
            .limit(limit)
        ).scalars().all()
        return InteractionDrilldownOut(
            items=[_to_out(row) for row in rows],
            total=int(total or 0),
            limit=limit,
            offset=offset,
            filters=InteractionDrilldownFiltersOut(
                from_ts=range_from.isoformat(),
                to_ts=range_to.isoformat(),
                queue_id=queue_id,
                channel=channel,
                agent_id=agent_id,
                status=status,
                q=normalized_q or None,
                sort_by=normalized_sort_by,
                sort_dir=normalized_sort_dir,
            ),
        )
    finally:
        session.close()


@app.get("/interactions/{interaction_id}", response_model=InteractionOut)
def get_interaction(interaction_id: str) -> InteractionOut:
    session = get_session()
    try:
        row = session.execute(
            select(Interaction).where(Interaction.interaction_id == interaction_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Interaction not found")
        return _to_out(row)
    finally:
        session.close()


@app.get("/interactions", response_model=list[InteractionOut])
def list_interactions(
    status: str | None = None,
    queue_id: str | None = None,
    assigned_to: str | None = None,
    limit: int = 100,
) -> list[InteractionOut]:
    session = get_session()
    try:
        stmt = select(Interaction).order_by(Interaction.id.desc())
        if status:
            stmt = stmt.where(Interaction.status == status)
        if queue_id:
            stmt = stmt.where(Interaction.queue_id == queue_id)
        if assigned_to:
            stmt = stmt.where(Interaction.assigned_to == assigned_to)

        rows = session.execute(stmt.limit(max(limit, 1))).scalars().all()
        return [_to_out(r) for r in rows]
    finally:
        session.close()


@app.patch("/interactions/{interaction_id}/assign", response_model=InteractionOut)
def assign_interaction(
    interaction_id: str,
    payload: AssignRequest,
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR)),
) -> InteractionOut:
    session = get_session()
    try:
        row = session.execute(
            select(Interaction).where(Interaction.interaction_id == interaction_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Interaction not found")
        row.assigned_to = payload.assignee
        row.status = "in_progress"
        row.updated_at = utc_now_iso()
        _push_event(session, row.interaction_id, "interaction.assigned", {"assignee": payload.assignee})
        _upsert_reporting_fact(session, row, source="interaction-service")
        if event_bus_enabled():
            append_outbox_event(
                session,
                event_type="interaction.assigned",
                producer_service="interaction-service",
                entity_type="interaction",
                entity_id=row.interaction_id,
                payload={
                    "event": "interaction.assigned",
                    "interaction_id": row.interaction_id,
                    "assignee": payload.assignee,
                    "queue_id": row.queue_id,
                    "channel": row.channel,
                    "status": row.status,
                    "updated_at": row.updated_at,
                },
            )
        session.commit()
        return _to_out(row)
    finally:
        session.close()


@app.patch("/interactions/{interaction_id}/status", response_model=InteractionOut)
def update_status(
    interaction_id: str,
    payload: StatusRequest,
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR, Role.OPERATOR)),
) -> InteractionOut:
    session = get_session()
    try:
        row = session.execute(
            select(Interaction).where(Interaction.interaction_id == interaction_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Interaction not found")
        row.status = payload.status
        row.updated_at = utc_now_iso()
        metadata = {"status": payload.status}
        if payload.resolved_first_contact is not None:
            metadata["resolved_first_contact"] = payload.resolved_first_contact
        _push_event(session, row.interaction_id, "interaction.status_changed", metadata)
        _upsert_reporting_fact(
            session,
            row,
            source="interaction-service",
            closed_at=row.updated_at if payload.status == "closed" else None,
            resolved_first_contact=payload.resolved_first_contact,
        )
        if payload.status == "closed" and event_bus_enabled():
            append_outbox_event(
                session,
                event_type="interaction.closed",
                producer_service="interaction-service",
                entity_type="interaction",
                entity_id=row.interaction_id,
                payload={
                    "event": "interaction.closed",
                    "interaction_id": row.interaction_id,
                    "queue_id": row.queue_id,
                    "channel": row.channel,
                    "assignee": row.assigned_to,
                    "status": row.status,
                    "resolved_first_contact": payload.resolved_first_contact,
                    "updated_at": row.updated_at,
                },
            )
        session.commit()
        return _to_out(row)
    finally:
        session.close()


@app.post("/interactions/{interaction_id}/escalate", response_model=InteractionOut)
def escalate(
    interaction_id: str,
    payload: EscalateRequest,
    _: dict = Depends(require_roles(Role.ADMIN, Role.SUPERVISOR, Role.OPERATOR)),
) -> InteractionOut:
    session = get_session()
    try:
        row = session.execute(
            select(Interaction).where(Interaction.interaction_id == interaction_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Interaction not found")
        row.queue_id = payload.target_queue_id
        row.status = "escalated"
        row.updated_at = utc_now_iso()
        _push_event(
            session,
            row.interaction_id,
            "interaction.escalated",
            {"target_queue_id": payload.target_queue_id},
        )
        _upsert_reporting_fact(session, row, source="interaction-service")
        if event_bus_enabled():
            append_outbox_event(
                session,
                event_type="interaction.escalated",
                producer_service="interaction-service",
                entity_type="interaction",
                entity_id=row.interaction_id,
                payload={
                    "event": "interaction.escalated",
                    "interaction_id": row.interaction_id,
                    "target_queue_id": payload.target_queue_id,
                    "queue_id": row.queue_id,
                    "channel": row.channel,
                    "status": row.status,
                    "updated_at": row.updated_at,
                },
            )
        session.commit()
        return _to_out(row)
    finally:
        session.close()


@app.get("/interactions/{interaction_id}/timeline")
def timeline(interaction_id: str) -> dict:
    session = get_session()
    try:
        row = session.execute(
            select(Interaction).where(Interaction.interaction_id == interaction_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Interaction not found")

        events = session.execute(
            select(InteractionTimeline)
            .where(InteractionTimeline.interaction_id == interaction_id)
            .order_by(InteractionTimeline.id.asc())
        ).scalars().all()
        return {
            "interaction_id": interaction_id,
            "events": [
                {
                    "timestamp": e.timestamp,
                    "action": e.action,
                    "metadata": json.loads(e.metadata_json or "{}"),
                }
                for e in events
            ],
        }
    finally:
        session.close()


@app.post("/interactions/{interaction_id}/timeline")
def append_timeline_event(
    interaction_id: str,
    payload: InteractionTimelineAppendIn,
    _: dict = Depends(require_roles(Role.ADMIN)),
) -> dict:
    session = get_session()
    try:
        row = session.execute(
            select(Interaction).where(Interaction.interaction_id == interaction_id)
        ).scalar_one_or_none()
        if not row:
            raise HTTPException(status_code=404, detail="Interaction not found")
        row.updated_at = utc_now_iso()
        _push_event(session, interaction_id, payload.action, payload.metadata)
        session.commit()
        return {"ok": True, "interaction_id": interaction_id, "action": payload.action}
    finally:
        session.close()
