from __future__ import annotations

import json
import os
from typing import Any
from urllib.parse import urlparse

from fastapi import HTTPException

from services.shared.models import BrowserSoftphoneConfigOut


def bool_env(name: str, default: bool) -> bool:
    raw = os.getenv(name)
    if raw is None:
        return default
    return raw.strip().lower() in {"1", "true", "yes", "on"}


def bridge_enabled() -> bool:
    return bool_env("ASTERISK_BRIDGE_ENABLED", False)


def ami_host() -> str:
    return os.getenv("ASTERISK_AMI_HOST", "").strip()


def ami_port() -> int:
    return int(os.getenv("ASTERISK_AMI_PORT", "5038"))


def ami_username() -> str:
    return os.getenv("ASTERISK_AMI_USERNAME", "").strip()


def ami_secret() -> str:
    return os.getenv("ASTERISK_AMI_SECRET", "").strip()


def ami_prefix() -> str:
    return os.getenv("ASTERISK_AMI_EVENT_PREFIX", "MVPCC").strip() or "MVPCC"


def poll_interval() -> float:
    try:
        value = float(os.getenv("ASTERISK_BRIDGE_POLL_INTERVAL_SECONDS", "1").strip())
    except ValueError:
        value = 1.0
    return max(value, 0.25)


def recent_calls_window_seconds() -> int:
    raw = os.getenv("ASTERISK_RECENT_CALLS_WINDOW_SECONDS", "120").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 120
    return max(value, 30)


def failed_retry_enabled() -> bool:
    return bool_env("ASTERISK_BRIDGE_FAILED_RETRY_ENABLED", True)


def failed_retry_interval_seconds() -> float:
    raw = os.getenv("ASTERISK_BRIDGE_FAILED_RETRY_INTERVAL_SECONDS", "5").strip()
    try:
        value = float(raw)
    except ValueError:
        value = 5.0
    return max(value, 1.0)


def failed_retry_batch_size() -> int:
    raw = os.getenv("ASTERISK_BRIDGE_FAILED_RETRY_BATCH_SIZE", "20").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 20
    return max(value, 1)


def reconcile_enabled() -> bool:
    return bool_env("ASTERISK_BRIDGE_RECONCILE_ENABLED", True)


def reconcile_stale_seconds() -> int:
    raw = os.getenv("ASTERISK_BRIDGE_RECONCILE_STALE_SECONDS", "12").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 30
    return max(value, 5)


def reconcile_settle_seconds() -> int:
    raw = os.getenv("ASTERISK_BRIDGE_RECONCILE_SETTLE_SECONDS", "6").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 8
    return max(value, 2)


def reconcile_scan_limit() -> int:
    raw = os.getenv("ASTERISK_BRIDGE_RECONCILE_SCAN_LIMIT", "20").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 20
    return max(value, 1)


def forward_timeout_seconds(default: float = 45.0) -> float:
    raw = os.getenv("ASTERISK_BRIDGE_FORWARD_TIMEOUT_SECONDS", str(default)).strip()
    try:
        value = float(raw)
    except ValueError:
        value = default
    return max(value, 5.0)


def forward_max_attempts() -> int:
    raw = os.getenv("ASTERISK_BRIDGE_FORWARD_MAX_ATTEMPTS", "3").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 3
    return max(value, 1)


def forward_retry_backoff_seconds() -> float:
    raw = os.getenv("ASTERISK_BRIDGE_FORWARD_RETRY_BACKOFF_SECONDS", "0.75").strip()
    try:
        value = float(raw)
    except ValueError:
        value = 0.75
    return max(value, 0.05)


def queue_map() -> dict[str, str]:
    raw = os.getenv("ASTERISK_QUEUE_MAP_JSON", "{}").strip()
    try:
        payload = json.loads(raw or "{}")
    except json.JSONDecodeError as exc:
        raise RuntimeError("Invalid ASTERISK_QUEUE_MAP_JSON") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("ASTERISK_QUEUE_MAP_JSON must be a JSON object")
    return {str(k): str(v) for k, v in payload.items() if str(k).strip() and str(v).strip()}


def sftp_enabled() -> bool:
    return bool(
        os.getenv("ASTERISK_SFTP_HOST", "").strip()
        and os.getenv("ASTERISK_SFTP_USERNAME", "").strip()
    )


def sftp_host() -> str:
    return os.getenv("ASTERISK_SFTP_HOST", "").strip()


def sftp_port() -> int:
    return int(os.getenv("ASTERISK_SFTP_PORT", "22"))


def sftp_username() -> str:
    return os.getenv("ASTERISK_SFTP_USERNAME", "").strip()


def sftp_password() -> str:
    return os.getenv("ASTERISK_SFTP_PASSWORD", "").strip()


def interaction_service_url() -> str:
    return os.getenv("INTERACTION_SERVICE_URL", "http://localhost:8004").rstrip("/")


def routing_service_url() -> str:
    return os.getenv("ROUTING_SERVICE_URL", "http://localhost:8003").rstrip("/")


def queue_level_map() -> dict[str, dict[str, str | None]]:
    raw = os.getenv("ASTERISK_QUEUE_LEVEL_MAP_JSON", "{}").strip()
    try:
        payload = json.loads(raw or "{}")
    except json.JSONDecodeError as exc:
        raise RuntimeError("Invalid ASTERISK_QUEUE_LEVEL_MAP_JSON") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("ASTERISK_QUEUE_LEVEL_MAP_JSON must be a JSON object")
    result: dict[str, dict[str, str | None]] = {}
    for queue_code, item in payload.items():
        key = str(queue_code or "").strip()
        if not key or not isinstance(item, dict):
            continue
        level = str(item.get("level") or "").strip()
        if level not in {"L2", "L3"}:
            continue
        result[key] = {"level": level, "tenant_id": str(item.get("tenant_id") or "").strip() or None}
    return result


def routing_level_for_queue_code(queue_code: str | None) -> str | None:
    entry = queue_level_map().get(str(queue_code or "").strip())
    return entry.get("level") if entry else None


def routing_tenant_for_queue_code(queue_code: str | None) -> str | None:
    entry = queue_level_map().get(str(queue_code or "").strip())
    return entry.get("tenant_id") if entry else None


def voice_adapter_service_url() -> str:
    return os.getenv("VOICE_ADAPTER_SERVICE_URL", "http://localhost:8006").rstrip("/")


def recording_service_url() -> str:
    return os.getenv("RECORDING_SERVICE_URL", "http://localhost:8013").rstrip("/")


def ivr_service_url() -> str:
    return os.getenv("IVR_SERVICE_URL", "http://localhost:8014").rstrip("/")


def ai_voice_runtime_service_url() -> str:
    return os.getenv("AI_VOICE_RUNTIME_SERVICE_URL", "http://localhost:8018").rstrip("/")


def ai_voice_enabled() -> bool:
    return bool_env("AI_VOICE_ENABLED", False)


def ai_voice_queue_config() -> dict[str, dict[str, Any]]:
    raw = os.getenv("AI_VOICE_QUEUE_CONFIG_JSON", "{}").strip()
    try:
        payload = json.loads(raw or "{}")
    except json.JSONDecodeError as exc:
        raise RuntimeError("Invalid AI_VOICE_QUEUE_CONFIG_JSON") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("AI_VOICE_QUEUE_CONFIG_JSON must be a JSON object")
    result: dict[str, dict[str, Any]] = {}
    for queue_code, item in payload.items():
        key = str(queue_code or "").strip()
        if not key or not isinstance(item, dict):
            continue
        result[key] = item
    return result


def _legacy_voice_start_profile(queue_code: str, *, stage: str | None, agent_profile: str) -> tuple[str | None, str]:
    if stage or agent_profile != "voice_support":
        return stage, agent_profile
    # Backward compatibility for deployed IVR AI queues that predate the
    # explicit voice_start stage/profile fields.
    if queue_code.startswith("ivr_") and "_ai_" in queue_code:
        return "voice_start", "voice_start"
    return stage, agent_profile


def ai_voice_config_for_queue(queue_code: str | None) -> dict[str, Any] | None:
    key = str(queue_code or "").strip()
    if not key or not ai_voice_enabled():
        return None
    item = ai_voice_queue_config().get(key)
    if not isinstance(item, dict):
        return None
    if str(item.get("mode") or "ai_first").strip().lower() != "ai_first":
        return None
    stage = str(item.get("stage") or "").strip() or None
    agent_profile = str(item.get("agent_profile") or "voice_support").strip() or "voice_support"
    stage, agent_profile = _legacy_voice_start_profile(
        key,
        stage=stage,
        agent_profile=agent_profile,
    )
    next_queue_code = str(item.get("next_queue_code") or "").strip() or None
    next_queue_id = str(item.get("next_queue_id") or "").strip() or None
    handoff_queue_code = str(item.get("handoff_queue_code") or next_queue_code or key).strip() or key
    handoff_queue_id = queue_map().get(handoff_queue_code)
    if next_queue_code and not next_queue_id:
        next_queue_id = queue_map().get(next_queue_code)
    if stage == "voice_start" and not handoff_queue_id:
        handoff_queue_id = next_queue_id
    return {
        "queue_code": key,
        "mode": "ai_first",
        "agent_profile": agent_profile,
        "language": str(item.get("language") or "").strip() or None,
        "stage": stage,
        "next_queue_code": next_queue_code,
        "next_queue_id": next_queue_id,
        "handoff_queue_code": handoff_queue_code,
        "handoff_queue_id": handoff_queue_id,
    }


def ivr_fastagi_enabled() -> bool:
    return bool_env("ASTERISK_IVR_FASTAGI_ENABLED", False)


def ivr_fastagi_host() -> str:
    return os.getenv("ASTERISK_IVR_FASTAGI_HOST", "127.0.0.1").strip() or "127.0.0.1"


def ivr_fastagi_port() -> int:
    raw = os.getenv("ASTERISK_IVR_FASTAGI_PORT", "4573").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 4573
    return max(value, 1)


def ivr_dtmf_timeout_seconds() -> int:
    raw = os.getenv("ASTERISK_IVR_DTMF_TIMEOUT_SECONDS", "5").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 5
    return max(value, 1)


def ivr_max_no_input_retries() -> int:
    raw = os.getenv("ASTERISK_IVR_MAX_NO_INPUT_RETRIES", "2").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 2
    return max(value, 0)


def ivr_max_invalid_retries() -> int:
    raw = os.getenv("ASTERISK_IVR_MAX_INVALID_RETRIES", "2").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 2
    return max(value, 0)


def ivr_call_link_wait_seconds() -> float:
    raw = os.getenv("ASTERISK_IVR_CALL_LINK_WAIT_SECONDS", "5").strip()
    try:
        value = float(raw)
    except ValueError:
        value = 5.0
    return max(value, 0.5)


def callcontrol_enabled() -> bool:
    return bool_env("ASTERISK_CALLCONTROL_ENABLED", False)


def callcontrol_action_timeout_seconds() -> float:
    raw = os.getenv("ASTERISK_CALLCONTROL_ACTION_TIMEOUT_SECONDS", "10").strip()
    try:
        value = float(raw)
    except ValueError:
        value = 10.0
    return max(value, 2.0)


def webrtc_enabled() -> bool:
    return bool_env("ASTERISK_WEBRTC_ENABLED", False)


def webrtc_ws_url() -> str:
    return os.getenv("ASTERISK_WEBRTC_WS_URL", "").strip()


def webrtc_ice_servers() -> list[dict[str, Any]]:
    raw = os.getenv("ASTERISK_WEBRTC_ICE_SERVERS_JSON", "[]").strip()
    try:
        payload = json.loads(raw or "[]")
    except json.JSONDecodeError as exc:
        raise RuntimeError("Invalid ASTERISK_WEBRTC_ICE_SERVERS_JSON") from exc
    if not isinstance(payload, list):
        raise RuntimeError("ASTERISK_WEBRTC_ICE_SERVERS_JSON must be a JSON array")
    result: list[dict[str, Any]] = []
    for item in payload:
        if isinstance(item, dict):
            result.append(item)
    return result


def operator_extension_map() -> dict[str, str]:
    raw = os.getenv("ASTERISK_OPERATOR_EXTENSION_MAP_JSON", "{}").strip()
    try:
        payload = json.loads(raw or "{}")
    except json.JSONDecodeError as exc:
        raise RuntimeError("Invalid ASTERISK_OPERATOR_EXTENSION_MAP_JSON") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("ASTERISK_OPERATOR_EXTENSION_MAP_JSON must be a JSON object")
    return {str(k).strip(): str(v).strip() for k, v in payload.items() if str(k).strip() and str(v).strip()}


def user_for_operator_extension(extension: str | None) -> str | None:
    target = str(extension or "").strip()
    if not target:
        return None
    matches = [user for user, mapped in operator_extension_map().items() if mapped == target]
    unique_matches = list(dict.fromkeys(matches))
    if len(unique_matches) == 1:
        return unique_matches[0]
    return None


def browser_sip_map() -> dict[str, dict[str, Any]]:
    raw = os.getenv("ASTERISK_BROWSER_SIP_MAP_JSON", "{}").strip()
    try:
        payload = json.loads(raw or "{}")
    except json.JSONDecodeError as exc:
        raise RuntimeError("Invalid ASTERISK_BROWSER_SIP_MAP_JSON") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("ASTERISK_BROWSER_SIP_MAP_JSON must be a JSON object")
    result: dict[str, dict[str, Any]] = {}
    for user, item in payload.items():
        if not str(user).strip() or not isinstance(item, dict):
            continue
        result[str(user).strip()] = item
    return result


def default_browser_sip_domain() -> str:
    ws_url = webrtc_ws_url()
    if ws_url:
        parsed = urlparse(ws_url)
        if parsed.hostname:
            return parsed.hostname
    return ami_host()


def browser_softphone_config_for_actor(actor: dict) -> BrowserSoftphoneConfigOut:
    user = str(actor.get("user") or "").strip()
    entry = browser_sip_map().get(user)
    if not entry:
        raise HTTPException(status_code=403, detail="Browser softphone is not configured for this user")

    operator_extension = (
        str(entry.get("operator_extension") or "").strip()
        or operator_extension_map().get(user, "")
    )
    authorization_username = (
        str(entry.get("authorization_username") or "").strip()
        or str(entry.get("username") or "").strip()
        or operator_extension
    )
    password = str(entry.get("password") or "").strip()
    display_name = str(entry.get("display_name") or actor.get("full_name") or user).strip() or user
    sip_uri = str(entry.get("sip_uri") or "").strip()
    if not sip_uri and authorization_username:
        domain = default_browser_sip_domain()
        if domain:
            sip_uri = f"sip:{authorization_username}@{domain}"

    return BrowserSoftphoneConfigOut(
        enabled=webrtc_enabled(),
        ws_url=webrtc_ws_url() or None,
        sip_uri=sip_uri or None,
        authorization_username=authorization_username or None,
        password=password or None,
        display_name=display_name or None,
        ice_servers=webrtc_ice_servers(),
        operator_extension=operator_extension or None,
    )


def transfer_target_map() -> dict[str, str]:
    raw = os.getenv("ASTERISK_TRANSFER_TARGET_MAP_JSON", "{}").strip()
    try:
        payload = json.loads(raw or "{}")
    except json.JSONDecodeError as exc:
        raise RuntimeError("Invalid ASTERISK_TRANSFER_TARGET_MAP_JSON") from exc
    if not isinstance(payload, dict):
        raise RuntimeError("ASTERISK_TRANSFER_TARGET_MAP_JSON must be a JSON object")
    return {str(k).strip(): str(v).strip() for k, v in payload.items() if str(k).strip() and str(v).strip()}


def claim_context() -> str:
    return os.getenv("ASTERISK_CALLCONTROL_CLAIM_CONTEXT", "mvpcc-claim").strip() or "mvpcc-claim"


def transfer_context() -> str:
    return os.getenv("ASTERISK_CALLCONTROL_TRANSFER_CONTEXT", "mvpcc-transfer").strip() or "mvpcc-transfer"


def bridge_auth_mode() -> str:
    mode = os.getenv("ASTERISK_BRIDGE_AUTH_MODE", "legacy_headers").strip().lower()
    if mode in {"legacy_headers", "bearer", "bearer_first"}:
        return mode
    return "legacy_headers"


def bridge_auth_fallback_legacy() -> bool:
    return bool_env("ASTERISK_BRIDGE_AUTH_FALLBACK_LEGACY", True)


def bridge_auth_user() -> str:
    return os.getenv("ASTERISK_BRIDGE_AUTH_USER", "asterisk-bridge").strip() or "asterisk-bridge"


def bridge_auth_role() -> str:
    return os.getenv("ASTERISK_BRIDGE_AUTH_ROLE", "admin").strip().lower() or "admin"


def bridge_auth_subject() -> str:
    return os.getenv("ASTERISK_BRIDGE_AUTH_SUBJECT", "svc:asterisk-bridge").strip() or "svc:asterisk-bridge"


def bridge_auth_token_ttl_seconds() -> int:
    raw = os.getenv("ASTERISK_BRIDGE_AUTH_TOKEN_TTL_SECONDS", "300").strip()
    try:
        value = int(raw)
    except ValueError:
        value = 300
    return max(value, 60)


def ai_voice_runtime_trusted_subjects() -> set[str]:
    raw = os.getenv(
        "AI_VOICE_RUNTIME_TRUSTED_SERVICE_SUBJECTS",
        "svc:ai-voice-runtime",
    )
    return {
        item.strip()
        for item in raw.split(",")
        if item.strip()
    }
