from __future__ import annotations

import json
import logging
from typing import Any

from fastapi import HTTPException
from sqlalchemy import select

from services.shared.core import new_id, utc_now_iso
from services.shared.db import get_session
from services.shared.models import (
    EscalationOut,
    EscalationRequestIn,
    VoiceAICallStateUpdateIn,
    VoiceAIHandoffRequestIn,
    VoiceAIMediaBridgeEventIn,
    VoiceAISummaryOut,
    VoiceAISummaryTranscriptSegmentOut,
    VoiceLiveCallOut,
)
from services.shared.sql_models import (
    AISessionRow,
    AsteriskCallLinkRow,
    EscalationRow,
    VoiceAISessionRow,
    VoiceTranscriptSegmentRow,
)
from services.shared.voice_transcripts import add_transcript_segment, next_transcript_sequence


LOGGER = logging.getLogger("uvicorn.error")


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

    return bridge_app


def _truncate(value: str | None, limit: int = 1000) -> str | None:
    raw = str(value or "").strip()
    if not raw:
        return None
    return raw[:limit]


def append_interaction_timeline(
    *,
    interaction_id: str | None,
    action: str,
    metadata: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
    bridge = _bridge_app()
    if not str(interaction_id or "").strip():
        return None
    return bridge._post_json(
        f"{bridge._interaction_service_url()}/interactions/{interaction_id}/timeline",
        {"action": action, "metadata": metadata or {}},
        timeout_seconds=bridge._callcontrol_side_effect_timeout_seconds(),
        max_attempts=bridge._callcontrol_side_effect_max_attempts(),
        retry_backoff_seconds=0.0,
    )


def start_voice_ai_session(
    *,
    call_id: str,
    linked_id: str | None,
    interaction_id: str,
    queue_id: str,
    queue_code: str,
    caller_number: str | None,
    caller_name: str | None,
    ai_config: dict[str, Any],
    extra_metadata: dict[str, Any] | None = None,
) -> dict[str, Any]:
    bridge = _bridge_app()
    metadata = {
        "queue_code": queue_code,
        "direction": "inbound",
        "stage": ai_config.get("stage"),
        "next_queue_code": ai_config.get("next_queue_code"),
        "next_queue_id": ai_config.get("next_queue_id"),
    }
    if isinstance(extra_metadata, dict):
        metadata.update(extra_metadata)
    return bridge._post_json(
        f"{bridge._ai_voice_runtime_service_url()}/internal/voice-ai/sessions",
        {
            "call_id": call_id,
            "linked_id": linked_id,
            "interaction_id": interaction_id,
            "queue_id": queue_id,
            "caller_number": caller_number,
            "caller_name": caller_name,
            "agent_profile": ai_config.get("agent_profile") or "voice_support",
            "language_hint": ai_config.get("language"),
            "handoff_queue_id": ai_config.get("handoff_queue_id") or queue_id,
            "metadata": metadata,
        },
        timeout_seconds=max(3.0, min(bridge._forward_timeout_seconds(), 12.0)),
        max_attempts=1,
        retry_backoff_seconds=0.0,
    )


def notify_voice_ai_telephony_event(
    *,
    voice_session_id: str | None,
    event_type: str,
    payload: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
    bridge = _bridge_app()
    session_id = str(voice_session_id or "").strip()
    if not session_id:
        return None
    return bridge._post_json(
        f"{bridge._ai_voice_runtime_service_url()}/internal/voice-ai/sessions/{session_id}/telephony-events",
        {"event_type": event_type, "payload": payload or {}},
        timeout_seconds=3.0,
        max_attempts=1,
        retry_backoff_seconds=0.0,
    )


def register_voice_ai_media_bridge(
    *,
    voice_session_id: str | None,
    event_type: str,
    media_uuid: str | None,
    call_id: str,
    linked_id: str | None,
    channel: str | None,
    service_address: str | None,
    reason: str | None = None,
) -> dict[str, Any] | None:
    bridge = _bridge_app()
    session_id = str(voice_session_id or "").strip()
    normalized_media_uuid = str(media_uuid or "").strip()
    if not session_id or not normalized_media_uuid:
        return None
    payload = VoiceAIMediaBridgeEventIn(
        event_type="ended" if str(event_type).strip() == "ended" else "requested",
        media_uuid=normalized_media_uuid,
        call_id=call_id,
        linked_id=linked_id,
        channel=channel,
        service_address=service_address,
        reason=reason,
    )
    return bridge._post_json(
        f"{bridge._ai_voice_runtime_service_url()}/internal/voice-ai/sessions/{session_id}/media-bridge",
        payload.model_dump(),
        timeout_seconds=3.0,
        max_attempts=1,
        retry_backoff_seconds=0.0,
    )


def trusted_voice_runtime_actor(actor: dict) -> bool:
    bridge = _bridge_app()
    return (
        str(actor.get("auth_source") or "") == "service"
        and str(actor.get("sub") or "") in bridge._ai_voice_runtime_trusted_subjects()
    )


def assert_trusted_voice_runtime_actor(actor: dict) -> None:
    if not trusted_voice_runtime_actor(actor):
        raise HTTPException(status_code=403, detail="Trusted Voice AI runtime subject required")


def _queue_code_for_queue_id(queue_id: str | None) -> str | None:
    bridge = _bridge_app()
    target = str(queue_id or "").strip()
    if not target:
        return None
    for queue_code, mapped_queue_id in bridge._queue_map().items():
        if str(mapped_queue_id).strip() == target:
            return str(queue_code).strip() or None
    return None


def _reserve_routing_agent(
    *,
    call_id: str,
    level: str,
    tenant_id: str | None,
    required_skills: list[str] | None = None,
) -> str | None:
    bridge = _bridge_app()
    try:
        response = bridge._post_json(
            f"{bridge._routing_service_url()}/internal/routing/reserve-agent",
            {
                "call_id": call_id,
                "level": level,
                "tenant_id": tenant_id,
                "required_skills": required_skills or [],
                "exclude_agent_ids": [],
            },
            timeout_seconds=bridge._callcontrol_side_effect_timeout_seconds(),
            max_attempts=1,
            retry_backoff_seconds=0.0,
        )
    except Exception:
        LOGGER.warning(
            "bridge.routing_reserve_failed call_id=%s level=%s tenant_id=%s",
            call_id,
            level,
            tenant_id,
        )
        return None
    extension = str((response or {}).get("extension") or "").strip()
    return extension or None


def _resolve_handoff_extension(
    target_queue_id: str | None,
    fallback_queue_code: str | None,
    *,
    call_id: str | None = None,
    target_level: str | None = None,
    tenant_id: str | None = None,
    required_skills: list[str] | None = None,
) -> tuple[str, str, str | None, str | None]:
    bridge = _bridge_app()
    queue_code = _queue_code_for_queue_id(target_queue_id) or str(fallback_queue_code or "").strip()
    if not queue_code:
        raise HTTPException(status_code=400, detail="Unable to resolve handoff queue_code")

    level = target_level or bridge._routing_level_for_queue_code(queue_code)
    if level and call_id:
        resolved_tenant_id = tenant_id if tenant_id is not None else bridge._routing_tenant_for_queue_code(queue_code)
        reserved_extension = _reserve_routing_agent(
            call_id=call_id,
            level=level,
            tenant_id=resolved_tenant_id,
            required_skills=required_skills,
        )
        if reserved_extension:
            return queue_code, reserved_extension, level, resolved_tenant_id
        raise HTTPException(status_code=409, detail=f"No available {level} agent right now")

    extension = bridge._transfer_target_map().get(queue_code)
    if not extension:
        raise HTTPException(
            status_code=400,
            detail=f"Unknown transfer queue_code: {queue_code}",
        )
    return queue_code, extension, None, None


def _resolve_handoff_channel(session, link: AsteriskCallLinkRow) -> str:
    bridge = _bridge_app()
    snapshot = bridge._build_call_channel_snapshot(
        session,
        link,
        operator_extension=link.operator_extension,
    )
    for channel in bridge._candidate_channels_for_call(
        session,
        link,
        operator_extension=link.operator_extension,
        prefer_operator=False,
        snapshot=snapshot,
    ):
        if (
            link.operator_extension
            and bridge._extract_extension_from_channel(channel) == str(link.operator_extension).strip()
        ):
            continue
        return channel
    channel = bridge._resolve_channel_name(
        session,
        link,
        prefer_operator=False,
        refresh=True,
        snapshot=snapshot,
    )
    if not channel:
        raise HTTPException(status_code=409, detail="Unable to resolve active channel for AI handoff")
    return channel


def _recover_channel_for_ended_call(session, link: AsteriskCallLinkRow) -> str | None:
    bridge = _bridge_app()
    snapshot = bridge._build_call_channel_snapshot(
        session,
        link,
        operator_extension=link.operator_extension,
    )
    for channel in bridge._candidate_channels_for_call(
        session,
        link,
        operator_extension=link.operator_extension,
        prefer_operator=False,
        snapshot=snapshot,
    ):
        if channel:
            now = utc_now_iso()
            link.status = "active"
            link.telephony_status = "connected"
            link.ended_at = None
            link.updated_at = now
            LOGGER.warning(
                "bridge.ai_handoff_revived call_id=%s interaction_id=%s channel=%s previous_ai_state=%s",
                link.call_id,
                link.interaction_id,
                channel,
                link.ai_state,
            )
            return channel
    return None


def _summary_text_from_payload(summary: dict[str, Any] | None) -> str | None:
    if not isinstance(summary, dict):
        return None
    for key in ("ai_outcome_text", "recommended_next_step", "customer_request_text"):
        value = _truncate(summary.get(key), 1200)
        if value:
            return value
    return None


def _voice_start_metadata(payload: dict[str, Any] | None) -> dict[str, str | None]:
    metadata = payload if isinstance(payload, dict) else {}
    return {
        "voice_start_language": _truncate(
            metadata.get("voice_start_language") or metadata.get("language"),
            16,
        ),
        "customer_name_status": _truncate(metadata.get("customer_name_status"), 32),
        "customer_name_value": _truncate(metadata.get("customer_name_value"), 256),
        "customer_name_source": _truncate(metadata.get("customer_name_source"), 32),
        "customer_name_resolved_at": _truncate(metadata.get("customer_name_resolved_at"), 64),
        "downstream_queue_code": _truncate(metadata.get("downstream_queue_code"), 64),
    }


def _apply_voice_start_metadata(target: Any, metadata: dict[str, Any] | None) -> None:
    payload = metadata if isinstance(metadata, dict) else {}
    values = _voice_start_metadata(payload)
    if hasattr(target, "voice_start_language") and (
        "voice_start_language" in payload or "language" in payload
    ):
        target.voice_start_language = values["voice_start_language"]
    if hasattr(target, "customer_name_status") and "customer_name_status" in payload:
        target.customer_name_status = values["customer_name_status"]
    if hasattr(target, "customer_name_value") and "customer_name_value" in payload:
        target.customer_name_value = values["customer_name_value"]
    if hasattr(target, "customer_name_source") and "customer_name_source" in payload:
        target.customer_name_source = values["customer_name_source"]
    if hasattr(target, "customer_name_resolved_at") and "customer_name_resolved_at" in payload:
        target.customer_name_resolved_at = values["customer_name_resolved_at"]


def _set_channel_variable(channel: str, name: str, value: str | None) -> None:
    bridge = _bridge_app()
    bridge._ami_action(
        "Setvar",
        {
            "Channel": channel,
            "Variable": name,
            "Value": str(value or ""),
        },
    )


def _set_handoff_channel_vars(
    channel: str,
    *,
    queue_code: str,
    reuse_existing_call: bool,
    metadata: dict[str, Any] | None,
) -> None:
    values = _voice_start_metadata(metadata)
    _set_channel_variable(channel, "MVPCC_START_LANGUAGE", values["voice_start_language"])
    _set_channel_variable(channel, "MVPCC_CUSTOMER_NAME_STATUS", values["customer_name_status"])
    _set_channel_variable(channel, "MVPCC_CUSTOMER_NAME_VALUE", values["customer_name_value"])
    _set_channel_variable(channel, "MVPCC_CUSTOMER_NAME_SOURCE", values["customer_name_source"])
    if reuse_existing_call:
        _set_channel_variable(channel, "MVPCC_AI_QUEUE_OVERRIDE", queue_code)
        _set_channel_variable(channel, "MVPCC_AI_REUSE_CALL", "1")
    else:
        _set_channel_variable(channel, "MVPCC_AI_QUEUE_OVERRIDE", "")
        _set_channel_variable(channel, "MVPCC_AI_REUSE_CALL", "")


def _start_ai_session_for_reuse_handoff(
    *,
    link: AsteriskCallLinkRow,
    queue_code: str,
    metadata: dict[str, Any] | None = None,
) -> tuple[dict[str, Any], str] | None:
    bridge = _bridge_app()
    if bridge._transfer_target_map().get(queue_code) != "7100":
        return None
    ai_config = bridge._ai_voice_config_for_queue(queue_code)
    if not ai_config:
        return None
    queue_id = str(bridge._queue_map().get(queue_code) or "").strip()
    if not queue_id:
        raise HTTPException(status_code=400, detail=f"Unknown AI queue mapping: {queue_code}")
    started = start_voice_ai_session(
        call_id=link.call_id,
        linked_id=link.linked_id,
        interaction_id=link.interaction_id,
        queue_id=queue_id,
        queue_code=queue_code,
        caller_number=link.caller_number,
        caller_name=link.caller_name,
        ai_config=ai_config,
        extra_metadata=metadata,
    )
    return started, queue_id


def request_handoff(call_id: str, body: VoiceAIHandoffRequestIn, actor: dict) -> VoiceLiveCallOut:
    bridge = _bridge_app()
    assert_trusted_voice_runtime_actor(actor)
    session = get_session()
    action = None
    try:
        link = bridge._load_live_call_or_404(session, call_id)
        if bridge._call_is_ended(link):
            revived_channel = _recover_channel_for_ended_call(session, link)
            if not revived_channel:
                LOGGER.warning(
                    "bridge.ai_handoff_rejected call_id=%s reason=call_ended interaction_id=%s ai_state=%s status=%s telephony_status=%s",
                    call_id,
                    link.interaction_id,
                    link.ai_state,
                    link.status,
                    link.telephony_status,
                )
                raise HTTPException(status_code=409, detail="Call is already ended")
        if str(link.interaction_id or "").strip() != str(body.interaction_id or "").strip():
            LOGGER.warning(
                "bridge.ai_handoff_rejected call_id=%s reason=interaction_mismatch link_interaction_id=%s body_interaction_id=%s ai_state=%s channel_name=%s",
                call_id,
                str(link.interaction_id or "").strip() or None,
                str(body.interaction_id or "").strip() or None,
                link.ai_state,
                link.channel_name,
            )
            raise HTTPException(status_code=409, detail="interaction_id mismatch")
        if (
            str(link.ai_state or "") in {"handoff_required", "human_owned"}
            and str(link.voice_session_id or "") == body.voice_session_id
        ):
            return bridge._live_call_to_out(session, link)

        voice_session = session.execute(
            select(VoiceAISessionRow).where(VoiceAISessionRow.session_id == body.voice_session_id)
        ).scalar_one_or_none()
        if voice_session is None:
            raise HTTPException(status_code=404, detail="Voice AI session not found")

        queue_code, target_extension, resolved_level, resolved_tenant_id = _resolve_handoff_extension(
            body.target_queue_id or voice_session.handoff_target_queue_id or link.queue_id,
            fallback_queue_code=_queue_code_for_queue_id(link.queue_id),
            call_id=call_id,
        )
        handoff_metadata = body.metadata or {}
        actor_user = str(actor.get("user") or actor.get("sub") or "ai-voice-runtime").strip()
        actor_role = str(actor.get("role") or "admin").strip() or "admin"
        action = bridge._create_action_log(
            session,
            call_id=call_id,
            interaction_id=link.interaction_id,
            action_type="ai-handoff",
            actor_user=actor_user,
            actor_role=actor_role,
            request_payload={
                "voice_session_id": body.voice_session_id,
                "ai_session_id": body.ai_session_id,
                "target_queue_id": body.target_queue_id or voice_session.handoff_target_queue_id or link.queue_id,
                "target_queue_code": queue_code,
                "resolved_extension": target_extension,
                "reason": body.reason,
                "metadata": handoff_metadata,
            },
        )
        channel = _resolve_handoff_channel(session, link)
        reuse_started: dict[str, Any] | None = None
        reuse_queue_id: str | None = None
        if target_extension == "7100":
            started = _start_ai_session_for_reuse_handoff(
                link=link,
                queue_code=queue_code,
                metadata=handoff_metadata,
            )
            if started is None:
                raise HTTPException(status_code=409, detail="AI reuse handoff is not configured for target queue")
            reuse_started, reuse_queue_id = started
        _set_handoff_channel_vars(
            channel,
            queue_code=queue_code,
            reuse_existing_call=target_extension == "7100",
            metadata=handoff_metadata,
        )
        LOGGER.warning(
            "bridge.ai_handoff_redirect call_id=%s interaction_id=%s source_channel=%s target_extension=%s target_queue_id=%s",
            call_id,
            link.interaction_id,
            channel,
            target_extension,
            body.target_queue_id or voice_session.handoff_target_queue_id or link.queue_id,
        )
        ami_result = bridge._ami_action(
            "Redirect",
            {
                "Channel": channel,
                "Context": bridge._transfer_context(),
                "Exten": target_extension,
                "Priority": 1,
            },
        )

        now = utc_now_iso()
        link.voice_session_id = (
            str((reuse_started or {}).get("voice_session_id") or body.voice_session_id or link.voice_session_id or "")
            or None
        )
        if target_extension == "7100":
            link.ai_session_id = str((reuse_started or {}).get("ai_session_id") or "").strip() or None
        else:
            link.ai_session_id = (
                str((reuse_started or {}).get("ai_session_id") or body.ai_session_id or link.ai_session_id or voice_session.ai_session_id or "")
                or None
            )
        link.ai_state = (
            _truncate((reuse_started or {}).get("status"), 32) or "greeting"
            if target_extension == "7100"
            else "handoff_required"
        )
        link.ai_handoff_reason = _truncate(body.reason, 4000)
        link.ai_last_model_at = now
        link.claimed_by_user = None
        link.claimed_at = None
        link.operator_extension = None
        if resolved_level:
            link.current_level = resolved_level
            link.tenant_id = resolved_tenant_id
        if reuse_queue_id:
            link.queue_code = queue_code
            link.queue_id = reuse_queue_id
        _apply_voice_start_metadata(link, handoff_metadata)
        link.updated_at = now

        if target_extension == "7100":
            voice_session.ai_session_id = str((reuse_started or {}).get("ai_session_id") or "").strip() or None
        else:
            voice_session.ai_session_id = (
                str((reuse_started or {}).get("ai_session_id") or body.ai_session_id or voice_session.ai_session_id or "")
                or None
            )
        voice_session.status = "handoff_requested"
        voice_session.handoff_reason = _truncate(body.reason, 4000)
        voice_session.handoff_target_queue_id = body.target_queue_id or voice_session.handoff_target_queue_id or link.queue_id
        _apply_voice_start_metadata(voice_session, handoff_metadata)
        voice_session.updated_at = now

        if body.summary:
            add_transcript_segment(
                session,
                session_id=voice_session.session_id,
                call_id=call_id,
                interaction_id=link.interaction_id,
                speaker="system",
                source_type="handoff_summary",
                text=_summary_text_from_payload(body.summary) or body.reason or "",
                payload=body.summary,
                created_at=now,
            )

        if str(link.ai_session_id or "").strip():
            ai_session = session.execute(
                select(AISessionRow).where(AISessionRow.session_id == link.ai_session_id)
            ).scalar_one_or_none()
            if ai_session:
                ai_session.status = "handoff_required"
                ai_session.handoff_reason = _truncate(body.reason, 4000)
                ai_session.summary_text = (
                    _summary_text_from_payload(body.summary)
                    or _truncate(body.reason, 2000)
                    or ai_session.summary_text
                )
                ai_session.updated_at = now

        bridge._set_action_result(
            session,
            action,
            result_status="ok",
            ami_action_id=str((ami_result or {}).get("action_id") or "") or None,
        )
        session.commit()

        try:
            append_interaction_timeline(
                interaction_id=link.interaction_id,
                action="ai.handoff_requested",
                metadata={
                    "call_id": call_id,
                    "voice_session_id": body.voice_session_id,
                    "ai_session_id": body.ai_session_id,
                    "reason": body.reason,
                    "target_queue_id": body.target_queue_id or voice_session.handoff_target_queue_id or link.queue_id,
                    **{key: value for key, value in _voice_start_metadata(handoff_metadata).items() if value},
                },
            )
        except Exception:
            pass
        return bridge._live_call_to_out(session, link)
    except HTTPException as exc:
        LOGGER.warning(
            "bridge.ai_handoff_http_error call_id=%s detail=%s body_interaction_id=%s body_target_queue_id=%s",
            call_id,
            exc.detail if isinstance(exc.detail, str) else str(exc.detail),
            str(body.interaction_id or "").strip() or None,
            str(body.target_queue_id or "").strip() or None,
        )
        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"AI handoff failed: {exc}") from exc
    finally:
        session.close()


def release_routing_agent(call_id: str) -> None:
    bridge = _bridge_app()
    bridge._post_json(
        f"{bridge._routing_service_url()}/internal/routing/release-agent",
        {"call_id": call_id},
        timeout_seconds=bridge._callcontrol_side_effect_timeout_seconds(),
        max_attempts=1,
        retry_backoff_seconds=0.0,
    )


def _escalation_to_out(row: EscalationRow) -> EscalationOut:
    return EscalationOut(
        escalation_id=row.escalation_id,
        call_id=row.call_id,
        tenant_id=row.tenant_id,
        from_level=row.from_level,
        to_level=row.to_level,
        reason_code=row.reason_code,
        required_skills=json.loads(row.required_skills_json or "[]"),
        priority=row.priority,
        topic=row.topic,
        summary=row.summary,
        status=row.status,
        assigned_agent_id=row.assigned_agent_id,
        requested_at=row.requested_at,
        connected_at=row.connected_at,
        completed_at=row.completed_at,
    )


def create_escalation(call_id: str, body: EscalationRequestIn, actor: dict) -> EscalationOut:
    bridge = _bridge_app()
    session = get_session()
    try:
        link = bridge._load_live_call_or_404(session, call_id)
        if bridge._call_is_ended(link):
            raise HTTPException(status_code=409, detail="Call is already ended")

        existing = session.execute(
            select(EscalationRow).where(
                EscalationRow.call_id == call_id,
                EscalationRow.status.in_(["requested", "ringing"]),
            )
        ).scalar_one_or_none()
        if existing is not None:
            raise HTTPException(status_code=409, detail="An active escalation already exists for this call")

        from_level = str(link.current_level or "L1")
        now = utc_now_iso()
        escalation = EscalationRow(
            escalation_id=new_id("esc"),
            call_id=call_id,
            tenant_id=link.tenant_id,
            from_level=from_level,
            to_level=body.target_level,
            reason_code=body.reason_code,
            required_skills_json=json.dumps(body.required_skills, ensure_ascii=False),
            priority=body.priority,
            topic=body.topic,
            summary=body.summary,
            status="requested",
            requested_at=now,
        )
        session.add(escalation)
        session.flush()

        channel = _resolve_handoff_channel(session, link)
        reserved_extension = _reserve_routing_agent(
            call_id=call_id,
            level=body.target_level,
            tenant_id=link.tenant_id,
            required_skills=body.required_skills,
        )
        if not reserved_extension:
            escalation.status = "failed"
            escalation.completed_at = now
            session.commit()
            raise HTTPException(status_code=409, detail=f"No available {body.target_level} agent right now")

        try:
            bridge._ami_action(
                "Redirect",
                {
                    "Channel": channel,
                    "Context": bridge._transfer_context(),
                    "Exten": reserved_extension,
                    "Priority": 1,
                },
            )
        except Exception as exc:
            try:
                bridge._release_routing_agent(call_id)
            except Exception:
                LOGGER.warning("bridge.escalation_release_agent_failed call_id=%s", call_id)
            escalation.status = "failed"
            escalation.completed_at = utc_now_iso()
            session.commit()
            raise HTTPException(status_code=502, detail=f"Failed to redirect call to agent: {exc}") from exc

        escalation.status = "ringing"
        escalation.assigned_agent_id = reserved_extension
        link.current_level = body.target_level
        link.required_skills_json = json.dumps(body.required_skills, ensure_ascii=False)
        link.priority = body.priority
        link.claimed_by_user = None
        link.claimed_at = None
        link.operator_extension = None
        link.updated_at = now
        session.commit()
        return _escalation_to_out(escalation)
    finally:
        session.close()


def update_call_ai_state(call_id: str, body: VoiceAICallStateUpdateIn, actor: dict) -> VoiceLiveCallOut:
    bridge = _bridge_app()
    assert_trusted_voice_runtime_actor(actor)
    session = get_session()
    try:
        link = bridge._load_live_call_or_404(session, call_id)
        now = utc_now_iso()
        if str(body.voice_session_id or "").strip():
            link.voice_session_id = body.voice_session_id
        if str(body.ai_session_id or "").strip():
            link.ai_session_id = body.ai_session_id
        link.ai_state = body.ai_state
        if body.handoff_reason is not None:
            link.ai_handoff_reason = _truncate(body.handoff_reason, 4000)
        _apply_voice_start_metadata(link, body.metadata)
        link.ai_last_model_at = now
        link.updated_at = now
        session.commit()
        if body.ai_state == "error":
            try:
                append_interaction_timeline(
                    interaction_id=link.interaction_id,
                    action="ai.error",
                    metadata={
                        "call_id": call_id,
                        "voice_session_id": body.voice_session_id or link.voice_session_id,
                        "ai_session_id": body.ai_session_id or link.ai_session_id,
                        "error": _truncate(body.handoff_reason, 1000),
                        **(body.metadata or {}),
                    },
                )
            except Exception:
                pass
        return bridge._live_call_to_out(session, link)
    finally:
        session.close()


def _next_transcript_sequence(session, session_id: str) -> int:
    return next_transcript_sequence(session, session_id)


def _segment_payload(row: VoiceTranscriptSegmentRow) -> dict[str, Any]:
    try:
        payload = json.loads(row.payload_json or "{}")
    except Exception:
        payload = {}
    return payload if isinstance(payload, dict) else {}


def _segment_delivery_state(row: VoiceTranscriptSegmentRow) -> str | None:
    payload = _segment_payload(row)
    raw = payload.get("delivery_state") or payload.get("delivery_status")
    if raw:
        return str(raw).strip().lower() or None
    if row.speaker == "assistant" and row.is_final and not row.barge_in_interrupted:
        return "delivered"
    return None


def _caller_low_signal_text(text: str | None) -> bool:
    normalized = " ".join(str(text or "").strip().lower().replace(",", " ").replace(".", " ").split())
    if not normalized:
        return True
    return normalized in {
        "\u0430\u0433\u0430",
        "\u0430\u043b\u043b\u043e",
        "\u0434\u0430",
        "\u0434\u043e\u0431\u0440\u044b\u0439 \u0434\u0435\u043d\u044c",
        "\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435",
        "\u043b\u0430\u0434\u043d\u043e",
        "\u043d\u0435\u0442",
        "\u043d\u0435\u0430",
        "\u043e\u0439",
        "\u043e\u043a",
        "\u043f\u0440\u0438\u0432\u0435\u0442",
        "\u0441\u043b\u044b\u0448\u043d\u043e",
        "\u0441\u043b\u044b\u0448\u0443",
        "\u0443\u0433\u0443",
        "\u0445\u043e\u0440\u043e\u0448\u043e",
        "\u044f\u0441\u043d\u043e",
    }


def _caller_segment_intent_bearing(row: VoiceTranscriptSegmentRow) -> bool:
    payload = _segment_payload(row)
    marker = payload.get("intent_bearing")
    if isinstance(marker, bool):
        return marker
    text = str(row.text or "").strip().lower()
    if _caller_low_signal_text(text):
        return False
    markers = (
        "\u0433\u0440\u0430\u0444\u0438\u043a",
        "\u0432\u0440\u0435\u043c\u044f \u0440\u0430\u0431\u043e\u0442\u044b",
        "\u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b",
        "\u0430\u0434\u0440\u0435\u0441",
        "\u0444\u0438\u043b\u0438\u0430\u043b",
        "\u0433\u043e\u0440\u043e\u0434",
        "\u0437\u0430\u044f\u0432\u043a",
        "\u0441\u0442\u0430\u0442\u0443\u0441",
        "\u0442\u0430\u0440\u0438\u0444",
        "\u0446\u0435\u043d\u0430",
        "\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c",
        "\u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440",
        "\u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440",
        "\u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442",
        "\u043e\u0448\u0438\u0431\u043a",
        "\u043f\u0440\u043e\u0431\u043b\u0435\u043c",
        "\u0445\u043e\u0447\u0443 \u0443\u0437\u043d\u0430\u0442\u044c",
        "\u043c\u043d\u0435 \u043d\u0430\u0434\u043e",
        "\u043c\u043d\u0435 \u043d\u0443\u0436\u043d\u043e",
    )
    return any(marker in text for marker in markers)


def _latest_customer_request_text(session, *, voice_session_id: str) -> str | None:
    rows = session.execute(
        select(VoiceTranscriptSegmentRow)
        .where(VoiceTranscriptSegmentRow.session_id == voice_session_id)
        .where(VoiceTranscriptSegmentRow.speaker == "caller")
        .where(VoiceTranscriptSegmentRow.is_final.is_(True))
        .order_by(VoiceTranscriptSegmentRow.sequence_no.desc(), VoiceTranscriptSegmentRow.id.desc())
    ).scalars().all()
    if not rows:
        return None
    for row in rows:
        text = _truncate(row.text, 4000)
        if text and _caller_segment_intent_bearing(row):
            return text
    for row in rows:
        text = _truncate(row.text, 4000)
        if text and not _caller_low_signal_text(text):
            return text
    return _truncate(rows[0].text, 4000)


def _latest_delivered_assistant_text(session, *, voice_session_id: str) -> str | None:
    rows = session.execute(
        select(VoiceTranscriptSegmentRow)
        .where(VoiceTranscriptSegmentRow.session_id == voice_session_id)
        .where(VoiceTranscriptSegmentRow.speaker == "assistant")
        .order_by(VoiceTranscriptSegmentRow.sequence_no.desc(), VoiceTranscriptSegmentRow.id.desc())
    ).scalars().all()
    for row in rows:
        if _segment_delivery_state(row) != "delivered":
            continue
        text = _truncate(row.text, 4000)
        if text:
            return text
    return None


def _latest_segment_text(
    session,
    *,
    voice_session_id: str,
    speaker: str,
    final_only: bool = False,
) -> str | None:
    normalized_speaker = str(speaker or "").strip().lower()
    if normalized_speaker == "caller":
        return _latest_customer_request_text(session, voice_session_id=voice_session_id)
    if normalized_speaker == "assistant":
        if final_only:
            return _latest_delivered_assistant_text(session, voice_session_id=voice_session_id)
        rows = session.execute(
            select(VoiceTranscriptSegmentRow)
            .where(VoiceTranscriptSegmentRow.session_id == voice_session_id)
            .where(VoiceTranscriptSegmentRow.speaker == "assistant")
            .order_by(VoiceTranscriptSegmentRow.id.desc())
        ).scalars().all()
        for row in rows:
            text = _truncate(row.text, 4000)
            if text:
                return text
        return None
    query = (
        select(VoiceTranscriptSegmentRow)
        .where(VoiceTranscriptSegmentRow.session_id == voice_session_id)
        .where(VoiceTranscriptSegmentRow.speaker == speaker)
    )
    if final_only:
        query = query.where(VoiceTranscriptSegmentRow.is_final.is_(True))
    row = session.execute(query.order_by(VoiceTranscriptSegmentRow.id.desc()).limit(1)).scalar_one_or_none()
    return _truncate(row.text if row else None, 4000)


def _summary_transcript_segments(
    session,
    *,
    voice_session_id: str,
) -> list[VoiceAISummaryTranscriptSegmentOut]:
    rows = session.execute(
        select(VoiceTranscriptSegmentRow)
        .where(VoiceTranscriptSegmentRow.session_id == voice_session_id)
        .order_by(VoiceTranscriptSegmentRow.sequence_no.asc(), VoiceTranscriptSegmentRow.id.asc())
    ).scalars().all()
    result: list[VoiceAISummaryTranscriptSegmentOut] = []
    for row in rows:
        text = _truncate(row.text, 2000)
        if not text:
            continue
        speaker = str(row.speaker or "").strip().lower()
        if speaker == "caller":
            if not row.is_final:
                continue
        elif speaker == "assistant":
            if _segment_delivery_state(row) != "delivered":
                continue
        else:
            continue
        result.append(
            VoiceAISummaryTranscriptSegmentOut(
                speaker=speaker,
                text=text,
                sequence_no=int(row.sequence_no or 0),
                source_type=str(row.source_type or "").strip() or "unknown",
                created_at=_truncate(row.created_at, 64) or utc_now_iso(),
                interrupted=bool(row.barge_in_interrupted),
            )
        )
    return result


def voice_ai_summary_for_call(call_id: str) -> VoiceAISummaryOut | None:
    session = get_session()
    try:
        link = session.execute(
            select(AsteriskCallLinkRow).where(AsteriskCallLinkRow.call_id == call_id)
        ).scalar_one_or_none()
        if link is None:
            raise HTTPException(status_code=404, detail="Live call not found")

        voice_session = None
        if str(link.voice_session_id or "").strip():
            voice_session = session.execute(
                select(VoiceAISessionRow).where(VoiceAISessionRow.session_id == link.voice_session_id)
            ).scalar_one_or_none()
        if voice_session is None:
            voice_session = session.execute(
                select(VoiceAISessionRow).where(VoiceAISessionRow.call_id == call_id)
            ).scalar_one_or_none()
        if voice_session is None:
            return None

        ai_session = None
        ai_session_id = (
            str(link.ai_session_id or "").strip()
            or str(voice_session.ai_session_id or "").strip()
        )
        if ai_session_id:
            ai_session = session.execute(
                select(AISessionRow).where(AISessionRow.session_id == ai_session_id)
            ).scalar_one_or_none()
        transcript_segments = _summary_transcript_segments(
            session,
            voice_session_id=voice_session.session_id,
        )

        customer_request_text = _latest_segment_text(
            session,
            voice_session_id=voice_session.session_id,
            speaker="caller",
        ) or "Последняя реплика клиента недоступна."
        assistant_reply_text = _latest_segment_text(
            session,
            voice_session_id=voice_session.session_id,
            speaker="assistant",
            final_only=True,
        )
        handoff_reason = (
            _truncate(voice_session.handoff_reason, 4000)
            or _truncate(ai_session.handoff_reason if ai_session else None, 4000)
            or _truncate(link.ai_handoff_reason, 4000)
            or ""
        )
        customer_name_status = (
            _truncate(voice_session.customer_name_status, 64)
            or _truncate(link.customer_name_status, 64)
            or None
        )
        customer_name_value = (
            _truncate(voice_session.customer_name_value, 256)
            or _truncate(link.customer_name_value, 256)
            or None
        )
        customer_name_source = (
            _truncate(voice_session.customer_name_source, 64)
            or _truncate(link.customer_name_source, 64)
            or None
        )
        voice_start_language = (
            _truncate(voice_session.voice_start_language, 16)
            or _truncate(link.voice_start_language, 16)
            or _truncate(voice_session.language, 16)
            or None
        )
        ai_outcome_text = (
            assistant_reply_text
            or _truncate(ai_session.summary_text if ai_session else None, 4000)
            or handoff_reason
        )
        if not ai_outcome_text and not handoff_reason and not transcript_segments:
            return None

        handoff_states = {"handoff_requested", "handoff_required", "human_owned"}
        is_handoff = (
            str(link.ai_state or "") in handoff_states
            or str(voice_session.status or "") in handoff_states
            or bool(handoff_reason)
        )
        unresolved_name = customer_name_status in {"name_not_obtained", "name_followup_required"}
        recommended_next_step = (
            "Проверьте контекст звонка и продолжайте разговор вручную."
            if is_handoff
            else "Продолжайте разговор, учитывая уже собранный AI контекст."
        )
        if is_handoff and unresolved_name:
            recommended_next_step = (
                "Проверьте контекст звонка, продолжайте разговор вручную и уточните имя клиента."
            )
        return VoiceAISummaryOut(
            call_id=call_id,
            session_id=ai_session.session_id if ai_session else None,
            voice_session_id=voice_session.session_id,
            status_label=(
                "AI передал звонок оператору"
                if is_handoff
                else "AI ответил клиенту"
            ),
            status_tone="handoff" if is_handoff else "answered",
            customer_name_status=customer_name_status,
            customer_name_value=customer_name_value,
            customer_name_source=customer_name_source,
            voice_start_language=voice_start_language,
            customer_request_text=customer_request_text,
            ai_outcome_text=ai_outcome_text or "AI обработал обращение без дополнительной сводки.",
            handoff_reason=handoff_reason,
            recommended_next_step=recommended_next_step,
            generated_at=(
                _truncate(link.ai_last_model_at, 64)
                or _truncate(voice_session.updated_at, 64)
                or _truncate(ai_session.updated_at if ai_session else None, 64)
                or utc_now_iso()
            ),
            transcript_segments=transcript_segments,
        )
    finally:
        session.close()
