from __future__ import annotations

from dataclasses import dataclass
import json
import socket
import sys
import time
from typing import Any

from sqlalchemy import select

from services.shared.core import new_id, utc_now_iso
from services.shared.sql_models import AsteriskCallLinkRow, AsteriskEventLogRow


def _bridge_app():
    return sys.modules["services.asterisk_bridge_service.app"]


def first_non_empty(*values: Any) -> str | None:
    for value in values:
        text = str(value or "").strip()
        if text:
            return text
    return None


def extract_call_id(payload: dict[str, Any]) -> str | None:
    return first_non_empty(
        payload.get("CallID"),
        payload.get("CallId"),
        payload.get("Callid"),
        payload.get("LinkedID"),
        payload.get("LinkedId"),
        payload.get("Linkedid"),
        payload.get("UniqueID"),
        payload.get("UniqueId"),
        payload.get("Uniqueid"),
    )


def extract_linked_id(payload: dict[str, Any], call_id: str | None = None) -> str | None:
    return first_non_empty(
        payload.get("LinkedID"),
        payload.get("LinkedId"),
        payload.get("Linkedid"),
        payload.get("CallID"),
        payload.get("CallId"),
        payload.get("Callid"),
        call_id,
    )


def resolve_channel_from_payload(payload: dict[str, Any]) -> str | None:
    return first_non_empty(
        payload.get("Channel"),
        payload.get("channel"),
        payload.get("InboundChannel"),
        payload.get("Inboundchannel"),
    )


def read_ami_frame(handle) -> dict[str, str] | None:
    fields: dict[str, str] = {}
    while True:
        try:
            raw = handle.readline()
        except TimeoutError:
            return {}
        except socket.timeout:
            return {}
        if raw == b"":
            return fields or None
        line = raw.decode("utf-8", errors="replace").strip("\r\n")
        if not line:
            return fields or {}
        if ":" not in line:
            continue
        key, value = line.split(":", 1)
        fields[key.strip()] = value.strip()


def send_ami_action(handle, fields: dict[str, str]) -> None:
    chunks = [f"{key}: {value}\r\n".encode("utf-8") for key, value in fields.items()]
    chunks.append(b"\r\n")
    handle.write(b"".join(chunks))
    handle.flush()


def ami_login(handle) -> None:
    bridge = _bridge_app()
    send_ami_action(
        handle,
        {
            "Action": "Login",
            "Username": bridge._ami_username(),
            "Secret": bridge._ami_secret(),
            "Events": "on",
        },
    )
    while True:
        frame = read_ami_frame(handle)
        if frame is None:
            raise RuntimeError("AMI connection closed during login")
        if not frame:
            continue
        if frame.get("Response"):
            if frame.get("Response", "").lower() != "success":
                raise RuntimeError(frame.get("Message", "AMI login failed"))
            return


def list_channels_via_coreshowchannels(*, call_id: str, linked_id: str | None) -> list[str] | None:
    bridge = _bridge_app()
    sock = None
    handle = None
    action_id = new_id("ami")
    try:
        sock = socket.create_connection((bridge._ami_host(), bridge._ami_port()), timeout=10)
        sock.settimeout(bridge._callcontrol_action_timeout_seconds())
        handle = sock.makefile("rwb")
        ami_login(handle)
        send_ami_action(
            handle,
            {
                "Action": "CoreShowChannels",
                "ActionID": action_id,
            },
        )

        candidates: list[str] = []
        started_at = time.time()
        while True:
            if (time.time() - started_at) > bridge._callcontrol_action_timeout_seconds():
                break
            frame = read_ami_frame(handle)
            if frame is None:
                break
            if not frame:
                continue
            if frame.get("Event") == "CoreShowChannel":
                uniqueid = first_non_empty(frame.get("Uniqueid"), frame.get("UniqueID"))
                frame_linked = first_non_empty(frame.get("Linkedid"), frame.get("LinkedID"))
                channel = first_non_empty(frame.get("Channel"))
                if not channel:
                    continue
                if uniqueid == call_id or (linked_id and frame_linked == linked_id) or frame_linked == call_id:
                    candidates.append(channel)
            if frame.get("Event") == "CoreShowChannelsComplete":
                break

        return candidates
    except Exception:
        return None
    finally:
        try:
            if handle is not None:
                handle.close()
        except Exception:
            pass
        try:
            if sock is not None:
                sock.close()
        except Exception:
            pass


def resolve_channel_via_coreshowchannels(*, call_id: str, linked_id: str | None) -> str | None:
    candidates = list_channels_via_coreshowchannels(call_id=call_id, linked_id=linked_id)
    if not candidates:
        return None
    for channel in candidates:
        if channel.startswith("PJSIP/"):
            return channel
    return candidates[0]


def extract_extension_from_channel(channel: str | None) -> str | None:
    value = str(channel or "").strip()
    if not value or "/" not in value:
        return None
    body = value.split("/", 1)[1]
    if "-" in body:
        body = body.split("-", 1)[0]
    return body.strip() or None


def operator_channel_for_extension(channels: list[str], operator_extension: str | None) -> str | None:
    prefix = str(operator_extension or "").strip()
    if not prefix:
        return None
    needle = f"PJSIP/{prefix}-"
    for channel in channels:
        if channel.startswith(needle):
            return channel
    return None


@dataclass(slots=True)
class CallChannelSnapshot:
    live_channels: list[str]
    lookup_available: bool
    operator_extension: str | None
    cached_channel: str | None
    operator_channel: str | None
    connected_channel: str | None
    started_channel: str | None


def latest_event_channel(session, *, call_id: str, event_name: str) -> str | None:
    row = session.execute(
        select(AsteriskEventLogRow)
        .where(AsteriskEventLogRow.call_id == call_id)
        .where(AsteriskEventLogRow.ami_event_name == event_name)
        .order_by(AsteriskEventLogRow.id.desc())
        .limit(1)
    ).scalar_one_or_none()
    if row is None:
        return None
    try:
        payload = json.loads(row.payload_json or "{}")
    except Exception:
        payload = {}
    return resolve_channel_from_payload(payload)


def build_call_channel_snapshot(
    session,
    link: AsteriskCallLinkRow,
    *,
    operator_extension: str | None = None,
) -> CallChannelSnapshot:
    bridge = _bridge_app()
    normalized_operator_extension = str(operator_extension or link.operator_extension or "").strip() or None
    resolved_live_channels = bridge._list_channels_via_coreshowchannels(call_id=link.call_id, linked_id=link.linked_id)
    live_channels = resolved_live_channels or []
    return CallChannelSnapshot(
        live_channels=live_channels,
        lookup_available=resolved_live_channels is not None,
        operator_extension=normalized_operator_extension,
        cached_channel=str(link.channel_name or "").strip() or None,
        operator_channel=operator_channel_for_extension(live_channels, normalized_operator_extension),
        connected_channel=latest_event_channel(session, call_id=link.call_id, event_name=f"{bridge._ami_prefix()}OperatorConnected"),
        started_channel=latest_event_channel(session, call_id=link.call_id, event_name=f"{bridge._ami_prefix()}CallStarted"),
    )


def candidate_channels_for_call(
    session,
    link: AsteriskCallLinkRow,
    *,
    operator_extension: str | None = None,
    prefer_operator: bool = False,
    snapshot: CallChannelSnapshot | None = None,
) -> list[str]:
    candidates: list[str] = []

    def _add(channel: str | None) -> None:
        value = str(channel or "").strip()
        if value and value not in candidates:
            candidates.append(value)

    snapshot = snapshot or build_call_channel_snapshot(session, link, operator_extension=operator_extension)
    operator_extension = snapshot.operator_extension

    if prefer_operator:
        _add(snapshot.operator_channel)
        if extract_extension_from_channel(snapshot.connected_channel) == operator_extension:
            _add(snapshot.connected_channel)

    _add(snapshot.cached_channel)
    _add(snapshot.operator_channel)
    _add(snapshot.connected_channel)
    _add(snapshot.started_channel)
    for channel in snapshot.live_channels:
        _add(channel)
    return candidates


def resolve_channel_name(
    session,
    link: AsteriskCallLinkRow,
    *,
    prefer_operator: bool = False,
    refresh: bool = False,
    snapshot: CallChannelSnapshot | None = None,
) -> str | None:
    bridge = _bridge_app()
    snapshot = snapshot or build_call_channel_snapshot(session, link)
    cached_channel = snapshot.cached_channel
    live_channels = snapshot.live_channels
    lookup_available = snapshot.lookup_available
    operator_channel = snapshot.operator_channel
    connected_channel = snapshot.connected_channel

    if not refresh and cached_channel and (not lookup_available or cached_channel in live_channels):
        if prefer_operator and operator_channel:
            cached_channel = operator_channel
        if cached_channel != link.channel_name:
            link.channel_name = cached_channel
            link.updated_at = utc_now_iso()
        return cached_channel

    connected_channel = latest_event_channel(session, call_id=link.call_id, event_name=f"{bridge._ami_prefix()}OperatorConnected")
    if connected_channel and (not lookup_available or connected_channel in live_channels):
        cached_channel = connected_channel
    elif prefer_operator and operator_channel:
        cached_channel = operator_channel
    elif cached_channel and lookup_available and live_channels and cached_channel in live_channels:
        pass
    elif operator_channel:
        cached_channel = operator_channel
    else:
        started_channel = latest_event_channel(session, call_id=link.call_id, event_name=f"{bridge._ami_prefix()}CallStarted")
        if started_channel and (not lookup_available or started_channel in live_channels):
            cached_channel = started_channel
        elif lookup_available and live_channels:
            cached_channel = bridge._resolve_channel_via_coreshowchannels(call_id=link.call_id, linked_id=link.linked_id)

    if cached_channel:
        link.channel_name = cached_channel
        link.updated_at = utc_now_iso()
    return cached_channel


def ami_action(action: str, params: dict[str, Any]) -> dict[str, Any]:
    bridge = _bridge_app()
    if not (bridge._ami_host() and bridge._ami_username() and bridge._ami_secret()):
        raise RuntimeError("AMI host/credentials are not configured")

    action_id = new_id("ami")
    sock = None
    handle = None
    try:
        sock = socket.create_connection((bridge._ami_host(), bridge._ami_port()), timeout=10)
        sock.settimeout(bridge._callcontrol_action_timeout_seconds())
        handle = sock.makefile("rwb")
        ami_login(handle)

        frame_fields = {
            "Action": action,
            "ActionID": action_id,
        }
        for key, value in params.items():
            if value is None:
                continue
            frame_fields[str(key)] = str(value)
        send_ami_action(handle, frame_fields)

        started_at = time.time()
        while True:
            if (time.time() - started_at) > bridge._callcontrol_action_timeout_seconds():
                raise RuntimeError(f"AMI action timeout: {action}")
            frame = read_ami_frame(handle)
            if frame is None:
                raise RuntimeError("AMI action connection closed")
            if not frame:
                continue
            if frame.get("Response"):
                if frame.get("ActionID") and frame.get("ActionID") != action_id:
                    continue
                if frame.get("Response", "").lower() != "success":
                    raise RuntimeError(frame.get("Message", f"AMI {action} failed"))
                return {
                    "action_id": action_id,
                    "response": frame,
                }
    finally:
        try:
            if handle is not None:
                handle.close()
        except Exception:
            pass
        try:
            if sock is not None:
                sock.close()
        except Exception:
            pass


def ami_loop(stop_event=None) -> None:
    bridge = _bridge_app()
    active_stop_event = stop_event or bridge._background_stop_event()
    while not active_stop_event.is_set():
        if not bridge._bridge_enabled():
            bridge._STATE.set_connected(False)
            if active_stop_event.wait(bridge._poll_interval()):
                break
            continue

        if not (bridge._ami_host() and bridge._ami_username() and bridge._ami_secret()):
            bridge._STATE.set_connected(False)
            bridge._STATE.set_error("AMI host/credentials are not configured")
            if active_stop_event.wait(max(bridge._poll_interval(), 2)):
                break
            continue

        sock = None
        handle = None
        try:
            sock = socket.create_connection((bridge._ami_host(), bridge._ami_port()), timeout=10)
            sock.settimeout(10)
            handle = sock.makefile("rwb")
            ami_login(handle)
            bridge._STATE.set_connected(True)

            while not active_stop_event.is_set():
                if bridge._STATE.take_reconnect():
                    raise RuntimeError("Reconnect requested")
                frame = read_ami_frame(handle)
                if frame is None:
                    raise RuntimeError("AMI connection closed")
                if not frame:
                    continue
                if frame.get("Event") != "UserEvent":
                    continue
                user_event = str(frame.get("UserEvent") or "").strip()
                if not user_event.startswith(bridge._ami_prefix()):
                    continue
                bridge._STATE.set_last_event()
                bridge._record_ami_payload(frame)
        except Exception as exc:
            bridge._STATE.set_connected(False)
            bridge._STATE.set_error(str(exc))
            if active_stop_event.wait(max(bridge._poll_interval(), 2)):
                break
        finally:
            try:
                if handle is not None:
                    handle.close()
            except Exception:
                pass
            try:
                if sock is not None:
                    sock.close()
            except Exception:
                pass
