from __future__ import annotations

from typing import Any

from fastapi import HTTPException
from sqlalchemy import select

from services.shared.core import Role, utc_now_iso
from services.shared.db import get_session
from services.shared.models import VoiceCallBlindTransferIn, VoiceCallClaimIn, VoiceLiveCallOut
from services.shared.sql_models import AsteriskCallActionLogRow, AsteriskCallLinkRow


def _bridge_app():
    import services.asterisk_bridge_service.app as bridge_app

    return bridge_app


def load_live_call_or_404(session, call_id: str) -> AsteriskCallLinkRow:
    bridge = _bridge_app()
    link = bridge._find_call_link(session, call_id)
    if link is None:
        raise HTTPException(status_code=404, detail="Live call not found")
    return link


def call_is_ended(link: AsteriskCallLinkRow) -> bool:
    if str(link.status or "").strip().lower() == "ended":
        return True
    return str(link.telephony_status or "").strip().lower() == "ended"


def resolve_operator_extension(*, actor: dict, requested_extension: str | None = None) -> str:
    bridge = _bridge_app()
    actor_user = str(actor.get("user") or "").strip()
    actor_role = str(actor.get("role") or "").strip().lower()
    mapping = bridge._operator_extension_map()
    mapped = mapping.get(actor_user)
    requested = str(requested_extension or "").strip()

    if actor_role == Role.OPERATOR.value:
        if not mapped:
            raise HTTPException(status_code=400, detail=f"No extension mapping for operator '{actor_user}'")
        if requested and requested != mapped:
            raise HTTPException(status_code=400, detail="Operator extension override is not allowed")
        return mapped

    if requested:
        return requested
    if mapped:
        return mapped
    raise HTTPException(status_code=400, detail="operator_extension is required for this role")


def resolve_transfer_extension(*, target_type: str, target_value: str) -> str:
    bridge = _bridge_app()
    raw_value = str(target_value or "").strip()
    if not raw_value:
        raise HTTPException(status_code=400, detail="target_value is required")
    if target_type == "extension":
        return raw_value
    mapped = bridge._transfer_target_map().get(raw_value)
    if not mapped:
        raise HTTPException(status_code=400, detail=f"Unknown transfer queue_code: {raw_value}")
    return mapped


def assert_callcontrol_enabled() -> None:
    bridge = _bridge_app()
    if not bridge._callcontrol_enabled():
        raise HTTPException(status_code=409, detail="Call control is disabled")


def assert_call_control_permissions(*, actor: dict, link: AsteriskCallLinkRow, action_type: str) -> None:
    bridge = _bridge_app()
    role = str(actor.get("role") or "").strip().lower()
    user = str(actor.get("user") or "").strip()
    if role != Role.OPERATOR.value:
        return
    claimed_by = str(link.claimed_by_user or "").strip()
    if action_type == "claim":
        if claimed_by and claimed_by != user:
            raise HTTPException(status_code=403, detail="Call already claimed by another operator")
        return
    mapped_extension = bridge._operator_extension_map().get(user)
    if mapped_extension and str(link.operator_extension or "").strip() == mapped_extension:
        return
    if claimed_by != user:
        raise HTTPException(status_code=403, detail="Operator can control only own claimed call")


def claim_live_call(call_id: str, body: VoiceCallClaimIn, actor: dict) -> VoiceLiveCallOut:
    bridge = _bridge_app()
    assert_callcontrol_enabled()
    session = get_session()
    action: AsteriskCallActionLogRow | None = None
    try:
        link = load_live_call_or_404(session, call_id)
        assert_call_control_permissions(actor=actor, link=link, action_type="claim")
        if call_is_ended(link):
            raise HTTPException(status_code=409, detail="Call is already ended")
        if not link.interaction_id:
            raise HTTPException(status_code=409, detail="Call has no linked interaction")

        actor_user = str(actor.get("user") or "").strip() or "unknown"
        actor_role = str(actor.get("role") or "").strip() or "unknown"
        operator_extension = resolve_operator_extension(actor=actor, requested_extension=body.operator_extension)
        action = bridge._create_action_log(
            session,
            call_id=call_id,
            interaction_id=link.interaction_id,
            action_type="claim",
            actor_user=actor_user,
            actor_role=actor_role,
            request_payload={"operator_extension": operator_extension},
        )

        if link.claimed_by_user == actor_user and link.telephony_status in {"claimed", "connected"}:
            bridge._set_action_result(
                session,
                action,
                result_status="ok",
                error="already_claimed_by_actor",
            )
            session.commit()
            session.refresh(link)
            return bridge._live_call_to_out(session, link)

        snapshot = bridge._build_call_channel_snapshot(session, link, operator_extension=operator_extension)
        operator_channel = snapshot.operator_channel
        connected_channel = snapshot.connected_channel
        already_connected = (
            link.telephony_status == "connected"
            or (connected_channel is not None and bridge._extract_extension_from_channel(connected_channel) == operator_extension)
        )
        channel = operator_channel or connected_channel or bridge._resolve_channel_name(session, link, refresh=True, snapshot=snapshot)
        if not channel:
            raise HTTPException(status_code=409, detail="Unable to resolve active channel for call")

        ami_result: dict[str, Any] | None = None
        claim_warning: str | None = None
        if already_connected:
            claim_warning = "already_connected_to_operator"
        elif not operator_channel:
            source_channel = None
            for candidate in bridge._candidate_channels_for_call(
                session,
                link,
                operator_extension=operator_extension,
                prefer_operator=False,
                snapshot=snapshot,
            ):
                if bridge._extract_extension_from_channel(candidate) != operator_extension:
                    source_channel = candidate
                    break
            if not source_channel:
                source_channel = bridge._resolve_channel_name(session, link, refresh=True, snapshot=snapshot)
            if not source_channel:
                raise HTTPException(status_code=409, detail="Unable to resolve source channel for claim")
            try:
                ami_result = bridge._ami_action(
                    "Redirect",
                    {
                        "Channel": source_channel,
                        "Context": bridge._claim_context(),
                        "Exten": operator_extension,
                        "Priority": 1,
                    },
                )
            except Exception as exc:
                refresh_snapshot = bridge._build_call_channel_snapshot(session, link, operator_extension=operator_extension)
                if refresh_snapshot.operator_channel:
                    channel = refresh_snapshot.operator_channel
                elif refresh_snapshot.connected_channel and bridge._extract_extension_from_channel(refresh_snapshot.connected_channel) == operator_extension:
                    channel = refresh_snapshot.connected_channel
                else:
                    raise
                claim_warning = f"redirect_skipped: {exc}"

        now = utc_now_iso()
        link.claimed_by_user = actor_user
        link.claimed_at = now
        link.operator_extension = operator_extension
        link.channel_name = channel
        link.telephony_status = "connected" if already_connected else "claimed"
        link.updated_at = now

        assign_error: str | None = None
        try:
            bridge._assign_interaction(interaction_id=link.interaction_id, assignee=actor_user)
        except Exception as exc:
            assign_error = str(exc)
        action_error = "; ".join(part for part in [claim_warning, assign_error] if part) or None

        bridge._set_action_result(
            session,
            action,
            result_status="ok",
            ami_action_id=str((ami_result or {}).get("action_id") or "") or None,
            error=action_error,
        )
        session.commit()
        session.refresh(link)
        return bridge._live_call_to_out(session, link)
    except HTTPException as exc:
        if action is not None:
            bridge._set_action_result(session, action, result_status="rejected", error=exc.detail if isinstance(exc.detail, str) else str(exc.detail))
            session.commit()
        raise
    except Exception as exc:
        if action is not None:
            bridge._set_action_result(session, action, result_status="failed", error=str(exc))
            session.commit()
        raise HTTPException(status_code=502, detail=f"Claim failed: {exc}") from exc
    finally:
        session.close()


def hangup_live_call(call_id: str, actor: dict) -> VoiceLiveCallOut:
    bridge = _bridge_app()
    assert_callcontrol_enabled()
    session = get_session()
    action: AsteriskCallActionLogRow | None = None
    try:
        link = load_live_call_or_404(session, call_id)
        assert_call_control_permissions(actor=actor, link=link, action_type="hangup")
        if call_is_ended(link):
            raise HTTPException(status_code=409, detail="Call is already ended")

        actor_user = str(actor.get("user") or "").strip() or "unknown"
        actor_role = str(actor.get("role") or "").strip() or "unknown"
        action = bridge._create_action_log(
            session,
            call_id=call_id,
            interaction_id=link.interaction_id,
            action_type="hangup",
            actor_user=actor_user,
            actor_role=actor_role,
            request_payload={},
        )
        snapshot = bridge._build_call_channel_snapshot(session, link, operator_extension=link.operator_extension)
        candidates = bridge._candidate_channels_for_call(
            session,
            link,
            operator_extension=link.operator_extension,
            prefer_operator=True,
            snapshot=snapshot,
        )
        if not candidates:
            raise HTTPException(status_code=409, detail="Unable to resolve active channel for call")
        ami_result = None
        channel = None
        successful_channels: list[str] = []
        last_error = None
        for candidate in candidates:
            try:
                result = bridge._ami_action("Hangup", {"Channel": candidate})
                if ami_result is None:
                    ami_result = result
                    channel = candidate
                successful_channels.append(candidate)
            except Exception as exc:
                last_error = exc
                message = str(exc)
                if "No such channel" in message or "Channel does not exist" in message:
                    continue
                raise
        if not successful_channels or ami_result is None or channel is None:
            remaining_channels = bridge._build_call_channel_snapshot(
                session,
                link,
                operator_extension=link.operator_extension,
            ).live_channels
            if not remaining_channels:
                now = utc_now_iso()
                link.telephony_status = "ended"
                link.status = "ended"
                link.ended_at = link.ended_at or now
                link.updated_at = now
                bridge._set_action_result(
                    session,
                    action,
                    result_status="ok",
                    error="channel_already_closed",
                )
                session.commit()
                session.refresh(link)
                return bridge._live_call_to_out(session, link)
            raise RuntimeError(str(last_error or "No active channel available for hangup"))

        now = utc_now_iso()
        link.telephony_status = "ended"
        link.status = "ended"
        link.ended_at = link.ended_at or now
        link.channel_name = successful_channels[0]
        link.updated_at = now
        bridge._set_action_result(
            session,
            action,
            result_status="ok",
            ami_action_id=str(ami_result.get("action_id") or "") or None,
        )
        session.commit()
        session.refresh(link)
        return bridge._live_call_to_out(session, link)
    except HTTPException as exc:
        if action is not None:
            bridge._set_action_result(session, action, result_status="rejected", error=exc.detail if isinstance(exc.detail, str) else str(exc.detail))
            session.commit()
        raise
    except Exception as exc:
        if action is not None:
            bridge._set_action_result(session, action, result_status="failed", error=str(exc))
            session.commit()
        raise HTTPException(status_code=502, detail=f"Hangup failed: {exc}") from exc
    finally:
        session.close()


def blind_transfer_live_call(call_id: str, body: VoiceCallBlindTransferIn, actor: dict) -> VoiceLiveCallOut:
    bridge = _bridge_app()
    assert_callcontrol_enabled()
    session = get_session()
    action: AsteriskCallActionLogRow | None = None
    try:
        link = load_live_call_or_404(session, call_id)
        assert_call_control_permissions(actor=actor, link=link, action_type="blind-transfer")
        if call_is_ended(link):
            raise HTTPException(status_code=409, detail="Call is already ended")

        actor_user = str(actor.get("user") or "").strip() or "unknown"
        actor_role = str(actor.get("role") or "").strip() or "unknown"
        target_extension = resolve_transfer_extension(target_type=body.target_type, target_value=body.target_value)
        action = bridge._create_action_log(
            session,
            call_id=call_id,
            interaction_id=link.interaction_id,
            action_type="blind-transfer",
            actor_user=actor_user,
            actor_role=actor_role,
            request_payload={
                "target_type": body.target_type,
                "target_value": body.target_value,
                "resolved_extension": target_extension,
            },
        )

        snapshot = bridge._build_call_channel_snapshot(session, link, operator_extension=link.operator_extension)
        channel = bridge._resolve_channel_name(session, link, prefer_operator=True, refresh=True, snapshot=snapshot)
        if not channel:
            raise HTTPException(status_code=409, detail="Unable to resolve active channel for call")
        ami_result = bridge._ami_action(
            "Redirect",
            {
                "Channel": channel,
                "Context": bridge._transfer_context(),
                "Exten": target_extension,
                "Priority": 1,
            },
        )

        bridge._emit_voice_event(
            event_type="call.transferred",
            call_id=call_id,
            interaction_id=link.interaction_id,
            payload={
                "source": "asterisk",
                "target_type": body.target_type,
                "target_value": body.target_value,
                "target_extension": target_extension,
                "actor_user": actor_user,
                "linked_id": link.linked_id,
            },
        )
        now = utc_now_iso()
        link.status = "ended"
        link.telephony_status = "ended"
        link.ended_at = link.ended_at or now
        link.updated_at = now
        bridge._set_action_result(
            session,
            action,
            result_status="ok",
            ami_action_id=str(ami_result.get("action_id") or "") or None,
        )
        session.commit()
        session.refresh(link)
        return bridge._live_call_to_out(session, link)
    except HTTPException as exc:
        if action is not None:
            bridge._set_action_result(session, action, result_status="rejected", error=exc.detail if isinstance(exc.detail, str) else str(exc.detail))
            session.commit()
        raise
    except Exception as exc:
        if action is not None:
            bridge._set_action_result(session, action, result_status="failed", error=str(exc))
            session.commit()
        raise HTTPException(status_code=502, detail=f"Blind transfer failed: {exc}") from exc
    finally:
        session.close()


def list_live_call_actions(call_id: str, limit: int, actor: dict) -> list[dict]:
    session = get_session()
    bridge = _bridge_app()
    try:
        role = str(actor.get("role") or "").strip().lower()
        if role == Role.OPERATOR.value:
            link = bridge._find_call_link(session, call_id)
            if link and str(link.claimed_by_user or "").strip() not in {"", str(actor.get("user") or "").strip()}:
                raise HTTPException(status_code=403, detail="Operator can view actions only for own or unclaimed call")
        rows = session.execute(
            select(AsteriskCallActionLogRow)
            .where(AsteriskCallActionLogRow.call_id == call_id)
            .order_by(AsteriskCallActionLogRow.id.desc())
        ).scalars().all()
        return [bridge._action_to_out(row) for row in rows[: max(limit, 1)]]
    finally:
        session.close()
