import json
import logging
import base64
import ipaddress
import re
from datetime import datetime
from typing import Optional
from urllib.error import URLError, HTTPError
from urllib.request import Request as UrlRequest, urlopen
from urllib.parse import urlsplit, urlunsplit

from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket, WebSocketDisconnect

from app.core.auth_service import AuthService
from app.core.auth_dependencies import require_permission
from app.core.config import settings
from app.core.database import execute_query, execute_query_single
from app.services.sms_service import SmsService

from .schemas import TelefoniCallLinkUpdate, TelefoniUserMappingUpdate, TelefoniClickToCallRequest, SmsSendRequest
from .service import TelefoniService
from .utils import (
    digits_only,
    extract_extension,
    ip_in_whitelist,
    normalize_external_number,
    is_outbound_call,
    normalize_e164,
    phone_suffix_8,
)
from .websocket import manager

logger = logging.getLogger(__name__)
router = APIRouter()


@router.post("/sms/send")
async def send_sms(payload: SmsSendRequest, request: Request):
    user_id = getattr(request.state, "user_id", None)
    logger.info("📨 SMS send requested by user_id=%s", user_id)

    contact_id = payload.contact_id
    if not contact_id:
        contact = TelefoniService.find_contact_by_phone(payload.to)
        contact_id = int(contact["id"]) if contact and contact.get("id") else None

    if not contact_id:
        raise HTTPException(status_code=400, detail="SMS skal knyttes til en kontakt")

    try:
        result = SmsService.send_sms(payload.to, payload.message, payload.sender)
        execute_query(
            """
            INSERT INTO sms_messages (kontakt_id, bruger_id, recipient, sender, message, status, provider_response)
            VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb)
            """,
            (
                contact_id,
                user_id,
                result.get("recipient") or payload.to,
                payload.sender or settings.SMS_SENDER,
                payload.message,
                "sent",
                json.dumps(result.get("result") or {}),
            ),
        )
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except RuntimeError as e:
        execute_query(
            """
            INSERT INTO sms_messages (kontakt_id, bruger_id, recipient, sender, message, status, provider_response)
            VALUES (%s, %s, %s, %s, %s, %s, %s::jsonb)
            """,
            (
                contact_id,
                user_id,
                payload.to,
                payload.sender or settings.SMS_SENDER,
                payload.message,
                "failed",
                json.dumps({"error": str(e)}),
            ),
        )
        raise HTTPException(status_code=502, detail=str(e))
    except Exception as e:
        logger.error("❌ SMS send failed: %s", e)
        raise HTTPException(status_code=500, detail="SMS send failed")

    return {
        "status": "ok",
        **result,
    }


def _get_client_ip(request: Request) -> str:
    cf_ip = request.headers.get("cf-connecting-ip")
    if cf_ip:
        return cf_ip.strip()

    true_client_ip = request.headers.get("true-client-ip")
    if true_client_ip:
        return true_client_ip.strip()

    xff = request.headers.get("x-forwarded-for")
    if xff:
        return xff.split(",")[0].strip()

    x_real_ip = request.headers.get("x-real-ip")
    if x_real_ip:
        return x_real_ip.strip()

    return request.client.host if request.client else ""


def _is_internal_bmc_ip(client_ip: str) -> bool:
    if not client_ip:
        return False
    try:
        ip_obj = ipaddress.ip_address(client_ip)
    except ValueError:
        return False
    return ip_obj in ipaddress.ip_network("172.16.31.0/24")


def _is_internal_bmc_supernet_ip(client_ip: str) -> bool:
    if not client_ip:
        return False
    try:
        ip_obj = ipaddress.ip_address(client_ip)
    except ValueError:
        return False
    return ip_obj in ipaddress.ip_network("172.16.0.0/12")


def _validate_yealink_request(request: Request, token: Optional[str]) -> None:
    env_secret = (getattr(settings, "TELEFONI_SHARED_SECRET", "") or "").strip()
    db_secret = (_get_setting_value("telefoni_shared_secret", "") or "").strip()
    accepted_tokens = {s for s in (env_secret, db_secret) if s}
    env_whitelist = (getattr(settings, "TELEFONI_IP_WHITELIST", "") or "").strip()
    db_whitelist = (_get_setting_value("telefoni_ip_whitelist", "") or "").strip()
    whitelist = ",".join([part for part in (env_whitelist, db_whitelist) if part])
    client_ip = _get_client_ip(request)
    path = request.url.path

    def _mask(value: Optional[str]) -> str:
        if not value:
            return "<empty>"
        stripped = value.strip()
        if len(stripped) <= 8:
            return "***"
        return f"{stripped[:4]}...{stripped[-4:]}"

    if not accepted_tokens and not whitelist:
        logger.error(
            "❌ Telefoni callback rejected path=%s reason=no_security_config ip=%s",
            path,
            client_ip,
        )
        raise HTTPException(status_code=403, detail="Telefoni callbacks not configured")

    if token and token.strip() in accepted_tokens:
        logger.debug("✅ Telefoni callback accepted path=%s auth=token ip=%s", path, client_ip)
        return

    if token and accepted_tokens:
        logger.warning(
            "⚠️ Telefoni callback token mismatch path=%s ip=%s provided=%s accepted_sources=%s",
            path,
            client_ip,
            _mask(token),
            "+".join([name for name, value in (("env", env_secret), ("db", db_secret)) if value]) or "none",
        )
    elif not token:
        logger.info("ℹ️ Telefoni callback without token path=%s ip=%s", path, client_ip)

    if whitelist:
        if ip_in_whitelist(client_ip, whitelist):
            logger.debug("✅ Telefoni callback accepted path=%s auth=ip_whitelist ip=%s", path, client_ip)
            return
        logger.warning(
            "⚠️ Telefoni callback IP not in whitelist path=%s ip=%s whitelist=%s",
            path,
            client_ip,
            whitelist,
        )
    else:
        logger.info("ℹ️ Telefoni callback whitelist not configured path=%s ip=%s", path, client_ip)

    # Safety fallback: allow callbacks from internal BMC network range.
    if _is_internal_bmc_supernet_ip(client_ip):
        logger.warning(
            "⚠️ Telefoni callback accepted via internal supernet fallback path=%s ip=%s",
            path,
            client_ip,
        )
        return

    logger.warning("❌ Telefoni callback forbidden path=%s ip=%s", path, client_ip)
    raise HTTPException(status_code=403, detail="Forbidden")


@router.get("/telefoni/established")
async def yealink_established(
    request: Request,
    callid: Optional[str] = Query(None),
    call_id: Optional[str] = Query(None),
    caller: Optional[str] = Query(None),
    callee: Optional[str] = Query(None),
    remote: Optional[str] = Query(None),
    local: Optional[str] = Query(None),
    active_user: Optional[str] = Query(None),
    called_number: Optional[str] = Query(None),
    token: Optional[str] = Query(None),
):
    """Yealink Action URL: Established"""
    _validate_yealink_request(request, token)

    resolved_callid = (callid or call_id or "").strip()
    if not resolved_callid:
        raise HTTPException(status_code=422, detail="Missing callid/call_id")

    def _sanitize(value: Optional[str]) -> Optional[str]:
        if value is None:
            return None
        v = value.strip()
        if not v:
            return None
        if v.startswith("$"):
            return None
        return v

    def _is_external_number(value: Optional[str]) -> bool:
        d = digits_only(value)
        return len(d) >= 8

    def _is_internal_number(value: Optional[str], local_ext: Optional[str]) -> bool:
        d = digits_only(value)
        if not d:
            return False
        local_d = digits_only(local_ext)
        if local_d and d.endswith(local_d):
            return True
        return len(d) <= 6

    local_value = _sanitize(local) or _sanitize(active_user)
    caller_value = _sanitize(caller) or _sanitize(remote)
    callee_value = _sanitize(callee)
    called_number_value = _sanitize(called_number)

    local_extension = extract_extension(local_value) or local_value

    is_outbound = False
    if called_number_value and _is_external_number(called_number_value):
        is_outbound = True
    elif caller_value and local_extension:
        if not _is_internal_number(caller_value, local_extension):
            is_outbound = is_outbound_call(caller_value, local_extension)

    direction = "outbound" if is_outbound else "inbound"
    candidates = [
        callee_value,
        called_number_value,
        _sanitize(remote),
        caller_value,
    ] if direction == "outbound" else [
        caller_value,
        _sanitize(remote),
        callee_value,
        called_number_value,
    ]

    ekstern_raw = None
    for candidate in candidates:
        if candidate and _is_external_number(candidate):
            ekstern_raw = candidate
            break

    if not ekstern_raw:
        for candidate in candidates:
            if candidate:
                ekstern_raw = candidate
                break
    ekstern_normalized = normalize_external_number(ekstern_raw)
    ekstern_e164 = normalize_e164(ekstern_normalized or ekstern_raw)
    ekstern_value = ekstern_normalized or ((ekstern_raw or "").strip() or None)

    user_ids = TelefoniService.find_user_by_extension(local_extension)

    kontakt = TelefoniService.find_contact_by_phone(ekstern_raw)
    kontakt_id = kontakt.get("id") if kontakt else None

    # Get extended contact details if we found a contact
    contact_details = {}
    if kontakt_id:
        contact_details = TelefoniService.get_contact_details(kontakt_id)

    payload = {
        "callid": resolved_callid,
        "call_id": call_id,
        "caller": caller_value,
        "callee": callee_value,
        "remote": remote,
        "local": local_value,
        "active_user": active_user,
        "called_number": called_number_value,
        "direction": direction,
        "client_ip": _get_client_ip(request),
        "user_agent": request.headers.get("user-agent"),
        "received_at": datetime.utcnow().isoformat(),
    }

    # Store call with first user_id (or None if no users)
    primary_user_id = user_ids[0] if user_ids else None
    row = TelefoniService.upsert_call(
        callid=resolved_callid,
        user_id=primary_user_id,
        direction=direction,
        ekstern_nummer=ekstern_value,
        intern_extension=(local_extension or "")[:16] or None,
        kontakt_id=kontakt_id,
        raw_payload=json.dumps(payload),
        started_at=datetime.utcnow(),
    )

    # Send websocket notification to ALL users with this extension
    if user_ids:
        call_data = {
            "call_id": str(row.get("id") or resolved_callid),
            "number": ekstern_e164 or (ekstern_raw or ""),
            "direction": direction,
            "contact": kontakt,
            "recent_cases": contact_details.get("recent_cases", []),
            "contact_cases": contact_details.get("contact_cases", []),
            "company_cases": contact_details.get("company_cases", []),
            "related_contacts": contact_details.get("related_contacts", []),
            "last_call": contact_details.get("last_call"),
        }
        for user_id in user_ids:
            await manager.send_to_user(user_id, "incoming_call", call_data)
        logger.info("📞 Telefoni notification sent to %d user(s) for extension=%s", len(user_ids), local_extension)
    else:
        logger.info("⚠️ Telefoni established: no mapped user for extension=%s (callid=%s)", local_extension, resolved_callid)

    return {"status": "ok"}


@router.get("/telefoni/terminated")
async def yealink_terminated(
    request: Request,
    callid: Optional[str] = Query(None),
    call_id: Optional[str] = Query(None),
    duration: Optional[str] = Query(None),
    call_duration: Optional[str] = Query(None),
    token: Optional[str] = Query(None),
):
    """Yealink Action URL: Terminated"""
    _validate_yealink_request(request, token)

    resolved_callid = (callid or call_id or "").strip()
    if not resolved_callid:
        raise HTTPException(status_code=422, detail="Missing callid/call_id")

    duration_raw = (duration or call_duration or "").strip() or None
    duration_value: Optional[int] = None
    if duration_raw:
        try:
            duration_value = int(duration_raw)
        except ValueError:
            # Accept common timer formats from PBX/phones: mm:ss or hh:mm:ss
            if ":" in duration_raw:
                parts = duration_raw.split(":")
                try:
                    nums = [int(p) for p in parts]
                    if len(nums) == 2:
                        duration_value = nums[0] * 60 + nums[1]
                    elif len(nums) == 3:
                        duration_value = nums[0] * 3600 + nums[1] * 60 + nums[2]
                except ValueError:
                    duration_value = None

            if duration_value is None:
                logger.info("⚠️ Telefoni terminated with unparseable duration='%s' (callid=%s)", duration_raw, resolved_callid)

    updated = TelefoniService.terminate_call(resolved_callid, duration_value)
    if not updated:
        logger.info("⚠️ Telefoni terminated without established (callid=%s)", resolved_callid)
    return {"status": "ok"}


@router.websocket("/telefoni/ws")
async def telefoni_ws(websocket: WebSocket):
    token = websocket.query_params.get("token")
    auth_header = (websocket.headers.get("authorization") or "").strip()
    if not token and auth_header.lower().startswith("bearer "):
        token = auth_header.split(" ", 1)[1].strip()
    if not token:
        token = (websocket.cookies.get("access_token") or "").strip() or None

    payload = AuthService.verify_token(token) if token else None
    if not payload:
        env_secret = (getattr(settings, "TELEFONI_SHARED_SECRET", "") or "").strip()
        db_secret = (_get_setting_value("telefoni_shared_secret", "") or "").strip()
        accepted_tokens = {s for s in (env_secret, db_secret) if s}
        if token and token.strip() in accepted_tokens:
            user_id_param = websocket.query_params.get("user_id")
            try:
                user_id = int(user_id_param) if user_id_param else None
            except (TypeError, ValueError):
                user_id = None

            if not user_id:
                logger.info("⚠️ Telefoni WS rejected: shared secret requires user_id")
                await websocket.close(code=1008)
                return
        else:
            logger.info("⚠️ Telefoni WS rejected: invalid or missing token")
            await websocket.close(code=1008)
            return
    else:
        user_id_value = payload.get("sub") or payload.get("user_id")
        try:
            user_id = int(user_id_value)
        except (TypeError, ValueError):
            logger.info("⚠️ Telefoni WS rejected: invalid user id in token")
            await websocket.close(code=1008)
            return

    await manager.connect(user_id, websocket)
    logger.info("✅ Telefoni WS connected for user_id=%s", user_id)
    try:
        while True:
            # Keep alive / ignore client messages
            await websocket.receive_text()
    except WebSocketDisconnect:
        logger.info("ℹ️ Telefoni WS disconnected for user_id=%s", user_id)
        await manager.disconnect(user_id, websocket)
    except Exception:
        logger.info("ℹ️ Telefoni WS disconnected (exception) for user_id=%s", user_id)
        await manager.disconnect(user_id, websocket)


@router.post("/telefoni/test-popup")
async def telefoni_test_popup(
    request: Request,
    token: Optional[str] = Query(None),
    user_id: Optional[int] = Query(None),
    extension: Optional[str] = Query(None),
):
    """Trigger test popup for currently authenticated user."""
    target_user_id = getattr(request.state, "user_id", None)

    env_secret = (getattr(settings, "TELEFONI_SHARED_SECRET", "") or "").strip()
    db_secret = (_get_setting_value("telefoni_shared_secret", "") or "").strip()
    accepted_tokens = {s for s in (env_secret, db_secret) if s}
    token_valid = bool(token and token.strip() in accepted_tokens)

    if not target_user_id:
        if not token_valid:
            raise HTTPException(status_code=401, detail="Not authenticated")

        if user_id:
            target_user_id = int(user_id)
        elif extension:
            # find_user_by_extension now returns list - take first match for test
            user_ids = TelefoniService.find_user_by_extension(extension)
            target_user_id = user_ids[0] if user_ids else None

    if not target_user_id:
        raise HTTPException(status_code=422, detail="Provide user_id or extension when using token auth")

    conn_count = await manager.connection_count_for_user(int(target_user_id))
    await manager.send_to_user(
        int(target_user_id),
        "incoming_call",
        {
            "call_id": f"test-{int(datetime.utcnow().timestamp())}",
            "number": "+4511223344",
            "direction": "inbound",
            "contact": {
                "id": None,
                "name": "Test popup",
                "company": "BMC Hub",
            },
            "recent_cases": [
                {"id": 1, "titel": "Test sag 1", "created_at": datetime.utcnow()},
                {"id": 2, "titel": "Test sag 2", "created_at": datetime.utcnow()},
            ],
            "last_call": {
                "started_at": datetime.utcnow(),
                "bruger_navn": "Test Medarbejder",
                "duration_sec": 125,
            },
        },
    )
    return {
        "status": "ok",
        "message": "Test popup event sent",
        "user_id": int(target_user_id),
        "ws_connections": conn_count,
        "auth_mode": "token" if token_valid and not getattr(request.state, "user_id", None) else "session",
    }


@router.get("/telefoni/users")
async def list_telefoni_users():
    """List users for log filtering (auth-protected by middleware)."""
    rows = execute_query(
        """
        SELECT user_id, username, full_name, telefoni_extension, telefoni_aktiv, telefoni_phone_ip, telefoni_phone_username
        FROM users
        ORDER BY COALESCE(full_name, username) ASC
        """,
        (),
    )
    return rows or []


@router.patch("/telefoni/admin/users/{user_id}", dependencies=[Depends(require_permission("users.manage"))])
async def update_telefoni_user_mapping(user_id: int, data: TelefoniUserMappingUpdate):
    existing = execute_query_single("SELECT user_id FROM users WHERE user_id = %s", (user_id,))
    if not existing:
        raise HTTPException(status_code=404, detail="User not found")

    rows = execute_query(
        """
        UPDATE users
        SET
            telefoni_extension = COALESCE(%s, telefoni_extension),
            telefoni_aktiv = COALESCE(%s, telefoni_aktiv),
            telefoni_phone_ip = COALESCE(%s, telefoni_phone_ip),
            telefoni_phone_username = COALESCE(%s, telefoni_phone_username),
            telefoni_phone_password = CASE
                WHEN %s IS NULL OR %s = '' THEN telefoni_phone_password
                ELSE %s
            END
        WHERE user_id = %s
        RETURNING user_id, username, full_name, telefoni_extension, telefoni_aktiv, telefoni_phone_ip, telefoni_phone_username
        """,
        (
            data.telefoni_extension,
            data.telefoni_aktiv,
            data.telefoni_phone_ip,
            data.telefoni_phone_username,
            data.telefoni_phone_password,
            data.telefoni_phone_password,
            data.telefoni_phone_password,
            user_id,
        ),
    )
    return rows[0] if rows else {"status": "ok"}


def _get_setting_value(key: str, default: Optional[str] = None) -> Optional[str]:
    row = execute_query_single("SELECT value FROM settings WHERE key = %s", (key,))
    if not row:
        return default
    value = row.get("value")
    if value is None or value == "":
        return default
    return str(value)


@router.post("/telefoni/click-to-call")
async def click_to_call(payload: TelefoniClickToCallRequest, request: Request):
    client_ip = _get_client_ip(request)
    if not _is_internal_bmc_ip(client_ip):
        logger.warning("⚠️ Click-to-call blocked for non-internal IP: %s", client_ip or "unknown")
        raise HTTPException(status_code=403, detail="Click-to-call is only available on internal network")

    enabled = (_get_setting_value("telefoni_click_to_call_enabled", "false") or "false").lower() == "true"
    if not enabled:
        raise HTTPException(status_code=400, detail="Click-to-call is disabled")

    template = _get_setting_value("telefoni_action_url_template", "") or ""
    if not template:
        raise HTTPException(status_code=400, detail="telefoni_action_url_template is not configured")

    number_normalized = normalize_e164(payload.number) or payload.number.strip()
    extension_value = (payload.extension or "").strip()
    phone_ip_value = ""
    phone_username_value = ""
    phone_password_value = ""

    if payload.user_id:
        user_row = execute_query_single(
            """
            SELECT telefoni_extension, telefoni_phone_ip, telefoni_phone_username, telefoni_phone_password
            FROM users
            WHERE user_id = %s
            """,
            (payload.user_id,),
        )
        if user_row:
            if not extension_value:
                extension_value = (user_row.get("telefoni_extension") or "").strip()
            phone_ip_value = (user_row.get("telefoni_phone_ip") or "").strip()
            phone_username_value = (user_row.get("telefoni_phone_username") or "").strip()
            phone_password_value = (user_row.get("telefoni_phone_password") or "").strip()

    if "{number}" not in template and "{raw_number}" not in template:
        raise HTTPException(status_code=400, detail="Template must contain {number} or {raw_number}")

    if "{phone_ip}" in template and not phone_ip_value:
        raise HTTPException(status_code=400, detail="Template requires {phone_ip}, but selected user has no phone IP")
    if "{phone_username}" in template and not phone_username_value:
        raise HTTPException(status_code=400, detail="Template requires {phone_username}, but selected user has no phone username")
    if "{phone_password}" in template and not phone_password_value:
        raise HTTPException(status_code=400, detail="Template requires {phone_password}, but selected user has no phone password")

    raw_number_clean = re.sub(r"\s+", "", payload.number.strip())
    resolved_url = (
        template
        .replace("{number}", number_normalized)
        .replace("{raw_number}", raw_number_clean)
        .replace("{extension}", extension_value)
        .replace("{phone_ip}", phone_ip_value)
        .replace("{phone_username}", phone_username_value)
        .replace("{phone_password}", phone_password_value)
    )

    auth_header: Optional[str] = None
    if "@" in resolved_url:
        parsed = urlsplit(resolved_url)
        if not parsed.scheme or not parsed.netloc:
            raise HTTPException(status_code=400, detail="Action URL template resolves to invalid URL")

        netloc = parsed.netloc
        if "@" in netloc:
            userinfo, host_part = netloc.rsplit("@", 1)
            if ":" in userinfo:
                username, password = userinfo.split(":", 1)
            else:
                username, password = userinfo, ""

            credentials = f"{username}:{password}".encode("utf-8")
            auth_header = "Basic " + base64.b64encode(credentials).decode("ascii")
            resolved_url = urlunsplit((parsed.scheme, host_part, parsed.path, parsed.query, parsed.fragment))

    logger.info("📞 Click-to-call trigger: number=%s extension=%s", number_normalized, extension_value or "-")

    try:
        request = UrlRequest(resolved_url, method="GET")
        if auth_header:
            request.add_header("Authorization", auth_header)
        with urlopen(request, timeout=8) as response:
            status = getattr(response, "status", 200)
    except HTTPError as e:
        logger.error("❌ Click-to-call HTTP error: %s", e)
        raise HTTPException(status_code=502, detail=f"Action URL returned HTTP {e.code}")
    except URLError as e:
        logger.error("❌ Click-to-call URL error: %s", e)
        raise HTTPException(status_code=502, detail="Could not reach Action URL")
    except Exception as e:
        logger.error("❌ Click-to-call failed: %s", e)
        raise HTTPException(status_code=500, detail="Click-to-call failed")

    display_url = resolved_url
    if auth_header:
        display_url = "[basic-auth] " + resolved_url

    return {
        "status": "ok",
        "action_url": display_url,
        "http_status": status,
        "number": number_normalized,
    }


@router.get("/telefoni/calls")
async def list_calls(
    user_id: Optional[int] = Query(None),
    date_from: Optional[str] = Query(None),
    date_to: Optional[str] = Query(None),
    without_case: bool = Query(False),
    limit: int = Query(200, ge=1, le=2000),
    offset: int = Query(0, ge=0),
):
    where = []
    params = []

    if user_id is not None:
        where.append("t.bruger_id = %s")
        params.append(user_id)
    if date_from:
        where.append("t.started_at >= %s")
        params.append(date_from)
    if date_to:
        where.append("t.started_at <= %s")
        params.append(date_to)
    if without_case:
        where.append("t.sag_id IS NULL")

    where_sql = ("WHERE " + " AND ".join(where)) if where else ""

    query = f"""
        SELECT
            t.id,
            t.callid,
            t.bruger_id,
            t.direction,
            t.ekstern_nummer,
            COALESCE(
                NULLIF(TRIM(t.ekstern_nummer), '')
            ) AS display_number,
            t.intern_extension,
            t.kontakt_id,
            t.sag_id,
            t.started_at,
            t.ended_at,
            COALESCE(
                t.duration_sec,
                CASE
                    WHEN t.started_at IS NOT NULL AND t.ended_at IS NOT NULL THEN GREATEST(EXTRACT(EPOCH FROM (t.ended_at - t.started_at))::int, 0)
                    ELSE NULL
                END
            ) AS duration_sec,
            t.created_at,
            u.username,
            u.full_name,
            CONCAT(COALESCE(c.first_name, ''), ' ', COALESCE(c.last_name, '')) AS contact_name,
            (
                SELECT cu.name
                FROM contact_companies cc
                JOIN customers cu ON cu.id = cc.customer_id
                WHERE cc.contact_id = c.id
                ORDER BY cc.is_primary DESC NULLS LAST, cc.id ASC
                LIMIT 1
            ) AS contact_company,
            s.titel AS sag_titel
        FROM telefoni_opkald t
        LEFT JOIN users u ON u.user_id = t.bruger_id
        LEFT JOIN contacts c ON c.id = t.kontakt_id
        LEFT JOIN sag_sager s ON s.id = t.sag_id
        {where_sql}
        ORDER BY t.started_at DESC
        LIMIT %s OFFSET %s
    """
    params.extend([limit, offset])

    rows = execute_query(query, tuple(params)) or []
    for row in rows:
        display_raw = row.get("display_number") or row.get("ekstern_nummer")
        repaired = normalize_external_number(display_raw)
        if repaired:
            row["display_number"] = repaired
    if rows:
        return rows

    # Fallback: legacy mission call history (read-only rows) for environments
    # where historical calls were stored before telefoni_opkald was populated.
    if user_id is not None:
        return []

    legacy_where = []
    legacy_params = []
    if date_from:
        legacy_where.append("m.started_at >= %s")
        legacy_params.append(date_from)
    if date_to:
        legacy_where.append("m.started_at <= %s")
        legacy_params.append(date_to)

    legacy_where_sql = ("WHERE " + " AND ".join(legacy_where)) if legacy_where else ""

    legacy_query = f"""
        SELECT
            -ROW_NUMBER() OVER (ORDER BY m.started_at DESC, m.call_id) AS id,
            m.call_id AS callid,
            NULL::INTEGER AS bruger_id,
            CASE
                WHEN LOWER(COALESCE(m.state, '')) IN ('outbound', 'udgaaende') THEN 'outbound'
                ELSE 'inbound'
            END AS direction,
            m.caller_number AS ekstern_nummer,
            m.caller_number AS display_number,
            NULL::VARCHAR AS intern_extension,
            NULL::INTEGER AS kontakt_id,
            NULL::INTEGER AS sag_id,
            m.started_at,
            m.ended_at,
            CASE
                WHEN m.started_at IS NOT NULL AND m.ended_at IS NOT NULL
                THEN GREATEST(EXTRACT(EPOCH FROM (m.ended_at - m.started_at))::int, 0)
                ELSE NULL
            END AS duration_sec,
            m.updated_at AS created_at,
            NULL::VARCHAR AS username,
            NULL::VARCHAR AS full_name,
            COALESCE(NULLIF(TRIM(m.contact_name), ''), NULL) AS contact_name,
            COALESCE(NULLIF(TRIM(m.company_name), ''), NULL) AS contact_company,
            NULL::VARCHAR AS sag_titel,
            'legacy_mission'::VARCHAR AS source
        FROM mission_call_state m
        {legacy_where_sql}
        ORDER BY m.started_at DESC, m.call_id
        LIMIT %s OFFSET %s
    """
    legacy_params.extend([limit, offset])

    try:
        legacy_rows = execute_query(legacy_query, tuple(legacy_params)) or []
    except Exception:
        legacy_rows = []

    if without_case:
        return [r for r in legacy_rows if not r.get("sag_id")]
    return legacy_rows


@router.patch("/telefoni/calls/{call_id}")
async def update_call_links(call_id: int, data: TelefoniCallLinkUpdate):
    existing = execute_query_single("SELECT id FROM telefoni_opkald WHERE id = %s", (call_id,))
    if not existing:
        raise HTTPException(status_code=404, detail="Call not found")

    fields = []
    params = []

    if "sag_id" in data.model_fields_set:
        fields.append("sag_id = %s")
        params.append(data.sag_id)
    if "kontakt_id" in data.model_fields_set:
        fields.append("kontakt_id = %s")
        params.append(data.kontakt_id)

    if not fields:
        raise HTTPException(status_code=400, detail="No fields provided")

    query = f"""
        UPDATE telefoni_opkald
        SET {", ".join(fields)}
        WHERE id = %s
        RETURNING *
    """
    params.append(call_id)

    rows = execute_query(query, tuple(params))
    return rows[0] if rows else {"status": "ok"}
