import json
import logging
from typing import Any, Dict, Optional

from app.core.database import execute_query, execute_query_single


logger = logging.getLogger(__name__)


class MissionService:
    MISSION_CASE_TYPES = {"ticket", "opgave", "ordre", "projekt", "service"}

    @staticmethod
    def _safe(label: str, func, default):
        try:
            return func()
        except Exception as exc:
            logger.error("❌ Mission state component failed: %s (%s)", label, exc)
            return default

    @staticmethod
    def _table_exists(table_name: str) -> bool:
        row = execute_query_single("SELECT to_regclass(%s) AS table_name", (f"public.{table_name}",))
        return bool(row and row.get("table_name"))

    @staticmethod
    def get_ring_timeout_seconds() -> int:
        raw = MissionService.get_setting_value("mission_call_ring_timeout_seconds", "180") or "180"
        try:
            value = int(raw)
        except (TypeError, ValueError):
            value = 180
        return max(30, min(value, 3600))

    @staticmethod
    def expire_stale_ringing_calls() -> None:
        if not MissionService._table_exists("mission_call_state"):
            return

        timeout_seconds = MissionService.get_ring_timeout_seconds()
        execute_query(
            """
            UPDATE mission_call_state
            SET state = 'hangup',
                ended_at = COALESCE(ended_at, NOW()),
                updated_at = NOW()
            WHERE state = 'ringing'
              AND started_at < (NOW() - make_interval(secs => %s))
            """,
            (timeout_seconds,),
        )

    @staticmethod
    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)

    @staticmethod
    def parse_json_setting(key: str, default: Any) -> Any:
        raw = MissionService.get_setting_value(key, None)
        if raw is None:
            return default
        try:
            return json.loads(raw)
        except Exception:
            return default

    @staticmethod
    def build_alert_key(service_name: str, customer_name: Optional[str]) -> str:
        customer_part = (customer_name or "").strip().lower() or "global"
        return f"{service_name.strip().lower()}::{customer_part}"

    @staticmethod
    def resolve_contact_context(caller_number: Optional[str]) -> Dict[str, Optional[str]]:
        if not caller_number:
            return {"contact_name": None, "company_name": None, "customer_tag": None}

        query = """
            SELECT
                c.id,
                c.first_name,
                c.last_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 company_name,
                (
                    SELECT t.name
                    FROM entity_tags et
                    JOIN tags t ON t.id = et.tag_id
                    WHERE et.entity_type = 'contact'
                      AND et.entity_id = c.id
                      AND LOWER(t.name) IN ('vip', 'serviceaftale', 'service agreement')
                    ORDER BY t.name
                    LIMIT 1
                ) AS customer_tag
            FROM contacts c
            WHERE RIGHT(regexp_replace(COALESCE(c.phone, ''), '\\D', '', 'g'), 8) = RIGHT(regexp_replace(%s, '\\D', '', 'g'), 8)
               OR RIGHT(regexp_replace(COALESCE(c.mobile, ''), '\\D', '', 'g'), 8) = RIGHT(regexp_replace(%s, '\\D', '', 'g'), 8)
            ORDER BY (
                SELECT COUNT(*)
                FROM sag_kontakter sk
                JOIN sag_sager s ON s.id = sk.sag_id
                WHERE sk.contact_id = c.id
                  AND sk.deleted_at IS NULL
                  AND s.deleted_at IS NULL
                  AND LOWER(COALESCE(s.status, '')) <> 'lukket'
            ) DESC,
            (
                SELECT MAX(t.started_at)
                FROM telefoni_opkald t
                WHERE t.kontakt_id = c.id
            ) DESC NULLS LAST,
            c.id ASC
            LIMIT 1
        """
        row = execute_query_single(query, (caller_number, caller_number))
        if not row:
            return {"contact_name": None, "company_name": None, "customer_tag": None}

        contact_name = f"{(row.get('first_name') or '').strip()} {(row.get('last_name') or '').strip()}".strip() or None
        return {
            "contact_name": contact_name,
            "company_name": row.get("company_name"),
            "customer_tag": row.get("customer_tag"),
        }

    @staticmethod
    def insert_event(
        *,
        event_type: str,
        title: str,
        severity: str = "info",
        source: Optional[str] = None,
        customer_name: Optional[str] = None,
        payload: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        if not MissionService._table_exists("mission_events"):
            logger.warning("Mission table missing: mission_events (event skipped)")
            return {}

        rows = execute_query(
            """
            INSERT INTO mission_events (event_type, severity, title, source, customer_name, payload)
            VALUES (%s, %s, %s, %s, %s, %s::jsonb)
            RETURNING id, event_type, severity, title, source, customer_name, payload, created_at
            """,
            (event_type, severity, title, source, customer_name, json.dumps(payload or {})),
        )
        return rows[0] if rows else {}

    @staticmethod
    def get_kpis() -> Dict[str, int]:
        query = """
            SELECT
                COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet') AS open_cases,
                COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) = 'åben' AND ansvarlig_bruger_id IS NULL) AS new_cases,
                COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND ansvarlig_bruger_id IS NULL) AS unassigned_cases,
                COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND deadline IS NOT NULL AND deadline::date = CURRENT_DATE) AS deadlines_today,
                COUNT(*) FILTER (WHERE deleted_at IS NULL AND LOWER(status) <> 'afsluttet' AND deadline IS NOT NULL AND deadline::date < CURRENT_DATE) AS overdue_deadlines
            FROM sag_sager
        """
        row = execute_query_single(query) or {}
        return {
            "open_cases": int(row.get("open_cases") or 0),
            "new_cases": int(row.get("new_cases") or 0),
            "unassigned_cases": int(row.get("unassigned_cases") or 0),
            "deadlines_today": int(row.get("deadlines_today") or 0),
            "overdue_deadlines": int(row.get("overdue_deadlines") or 0),
        }

    @staticmethod
    def get_employee_deadlines() -> list[Dict[str, Any]]:
        rows = execute_query(
            """
            SELECT
                COALESCE(u.full_name, u.username, 'Ukendt') AS employee_name,
                COUNT(*) FILTER (WHERE s.deadline::date = CURRENT_DATE) AS deadlines_today,
                COUNT(*) FILTER (WHERE s.deadline::date < CURRENT_DATE) AS overdue_deadlines
            FROM sag_sager s
            LEFT JOIN users u ON u.user_id = s.ansvarlig_bruger_id
            WHERE s.deleted_at IS NULL
              AND LOWER(s.status) <> 'afsluttet'
              AND s.deadline IS NOT NULL
            GROUP BY COALESCE(u.full_name, u.username, 'Ukendt')
            HAVING COUNT(*) FILTER (WHERE s.deadline::date = CURRENT_DATE) > 0
               OR COUNT(*) FILTER (WHERE s.deadline::date < CURRENT_DATE) > 0
            ORDER BY overdue_deadlines DESC, deadlines_today DESC, employee_name ASC
            """
        ) or []
        return [
            {
                "employee_name": row.get("employee_name"),
                "deadlines_today": int(row.get("deadlines_today") or 0),
                "overdue_deadlines": int(row.get("overdue_deadlines") or 0),
            }
            for row in rows
        ]

    @staticmethod
    def get_active_calls() -> list[Dict[str, Any]]:
        if not MissionService._table_exists("mission_call_state"):
            logger.warning("Mission table missing: mission_call_state (active calls unavailable)")
            return []

        MissionService.expire_stale_ringing_calls()
        rows = execute_query(
            """
            SELECT call_id, queue_name, caller_number, contact_name, company_name, customer_tag, state, started_at, answered_at, ended_at, updated_at
            FROM mission_call_state
            WHERE state = 'ringing'
            ORDER BY started_at DESC
            """
        )
        return rows or []

    @staticmethod
    def get_active_alerts() -> list[Dict[str, Any]]:
        if not MissionService._table_exists("mission_uptime_alerts"):
            logger.warning("Mission table missing: mission_uptime_alerts (active alerts unavailable)")
            return []

        rows = execute_query(
            """
            SELECT alert_key, service_name, customer_name, status, is_active, started_at, resolved_at, updated_at
            FROM mission_uptime_alerts
            WHERE is_active = TRUE
            ORDER BY started_at ASC NULLS LAST
            """
        )
        return rows or []

    @staticmethod
    def get_live_feed(limit: int = 20) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("mission_events"):
            logger.warning("Mission table missing: mission_events (live feed unavailable)")
            return []

        rows = execute_query(
            """
            SELECT id, event_type, severity, title, source, customer_name, payload, created_at
            FROM mission_events
            ORDER BY created_at DESC
            LIMIT %s
            """,
            (limit,),
        )
        return rows or []

    @staticmethod
    def _normalize_case_type(template_key: Optional[str]) -> str:
        key = (template_key or "").strip().lower()
        if not key:
            return "opgave"

        if "ticket" in key:
            return "ticket"
        if "ordre" in key or "order" in key:
            return "ordre"
        if "projekt" in key or "project" in key or "pipeline" in key:
            return "projekt"
        if "service" in key or "support" in key:
            return "service"
        if "opgave" in key or "task" in key:
            return "opgave"
        return "opgave"

    @staticmethod
    def get_important_cases(limit: int = 80) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("sag_sager"):
            logger.warning("Mission table missing: sag_sager (important cases unavailable)")
            return []

        rows = execute_query(
            """
            SELECT
                s.id,
                s.titel,
                s.status,
                s.priority,
                s.deadline,
                s.template_key,
                s.created_at,
                COALESCE(c.name, 'Ukendt kunde') AS customer_name
            FROM sag_sager s
            LEFT JOIN customers c ON c.id = s.customer_id
            WHERE s.deleted_at IS NULL
              AND LOWER(COALESCE(s.status, '')) <> 'afsluttet'
            ORDER BY
                CASE LOWER(COALESCE(s.priority::text, ''))
                    WHEN 'kritisk' THEN 5
                    WHEN 'critical' THEN 5
                    WHEN 'høj' THEN 4
                    WHEN 'hoj' THEN 4
                    WHEN 'high' THEN 4
                    WHEN 'medium' THEN 3
                    WHEN 'normal' THEN 2
                    WHEN 'lav' THEN 1
                    WHEN 'low' THEN 1
                    ELSE 2
                END DESC,
                s.deadline ASC NULLS LAST,
                s.created_at DESC
            LIMIT %s
            """,
            (limit,),
        ) or []

        result: list[Dict[str, Any]] = []
        for row in rows:
            item = dict(row)
            normalized_type = MissionService._normalize_case_type(item.get("template_key"))
            if normalized_type not in MissionService.MISSION_CASE_TYPES:
                normalized_type = "opgave"
            item["case_type"] = normalized_type
            result.append(item)
        return result

    @staticmethod
    def _project_risk_level_from_score(score: int) -> str:
        if score <= 29:
            return "low"
        if score <= 69:
            return "medium"
        if score <= 119:
            return "high"
        return "critical"

    @staticmethod
    def _compute_project_risk(project_row: Dict[str, Any]) -> Dict[str, Any]:
        score = int(project_row.get("score") or 0)
        factors: list[str] = []

        overdue_tasks = int(project_row.get("overdue_tasks") or 0)
        open_blockers = int(project_row.get("open_blockers") or 0)
        overdue_milestones = int(project_row.get("overdue_milestones") or 0)

        if overdue_tasks > 0:
            score += min(overdue_tasks * 8, 40)
            factors.append("overdue_tasks")
        if overdue_milestones > 0:
            score += min(overdue_milestones * 10, 40)
            factors.append("overdue_milestones")
        if open_blockers > 0:
            score += min(open_blockers * 15, 60)
            factors.append("open_blockers")

        return {
            "risk_score": score,
            "risk_level": MissionService._project_risk_level_from_score(score),
            "risk_factors": factors,
        }

    @staticmethod
    def get_project_workload(limit: int = 100) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("mission_projects"):
            return []

        rows = execute_query(
            """
            SELECT
                p.id,
                p.name,
                COUNT(*) FILTER (
                    WHERE s.id IS NOT NULL
                      AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
                ) AS open_tasks,
                COUNT(*) FILTER (
                    WHERE s.id IS NOT NULL
                      AND s.deadline IS NOT NULL
                      AND s.deadline::date < CURRENT_DATE
                      AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
                ) AS overdue_tasks,
                COUNT(*) FILTER (
                    WHERE s.id IS NOT NULL
                      AND s.ansvarlig_bruger_id IS NULL
                      AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
                ) AS unassigned_tasks
            FROM mission_projects p
            LEFT JOIN sag_sager s ON s.project_id = p.id AND s.deleted_at IS NULL
            GROUP BY p.id, p.name
            ORDER BY overdue_tasks DESC, unassigned_tasks DESC, open_tasks DESC, p.id DESC
            LIMIT %s
            """,
            (limit,),
        ) or []

        return [dict(row) for row in rows]

    @staticmethod
    def _get_projects_from_cases(limit: int = 120) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("sag_sager"):
            return []

        case_rows = execute_query(
            """
            SELECT
                s.id,
                s.titel AS name,
                s.beskrivelse AS description,
                LOWER(COALESCE(s.status, 'active')) AS status,
                0 AS score,
                s.start_date AS started_at,
                s.deadline AS ended_at,
                s.created_at AS updated_at,
                0 AS active_milestones,
                0 AS overdue_milestones,
                0 AS open_blockers,
                1 AS open_tasks,
                CASE WHEN s.deadline IS NOT NULL AND s.deadline::date = CURRENT_DATE THEN 1 ELSE 0 END AS due_today_tasks,
                CASE
                    WHEN s.deadline IS NOT NULL
                     AND s.deadline::date < CURRENT_DATE
                     AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
                    THEN 1
                    ELSE 0
                END AS overdue_tasks
            FROM sag_sager s
            WHERE s.deleted_at IS NULL
              AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
              AND (
                                LOWER(COALESCE(s.template_key, '')) IN ('projekt', 'project')
                                OR LOWER(COALESCE(s.project_task_type, '')) = 'project'
                                OR s.project_id IS NOT NULL
              )
            ORDER BY s.deadline ASC NULLS LAST, s.created_at DESC
            LIMIT %s
            """,
            (limit,),
        ) or []

        fallback: list[Dict[str, Any]] = []
        for row in case_rows:
            item = dict(row)
            item.update(MissionService._compute_project_risk(item))
            fallback.append(item)
        return fallback

    @staticmethod
    def get_projects(limit: int = 120) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("mission_projects"):
            return MissionService._get_projects_from_cases(limit)

        rows = execute_query(
            """
            SELECT
                p.id,
                p.name,
                p.description,
                p.status,
                p.score,
                p.started_at,
                p.ended_at,
                p.updated_at,
                COUNT(DISTINCT m.id) FILTER (
                    WHERE m.status NOT IN ('completed', 'cancelled')
                ) AS active_milestones,
                COUNT(DISTINCT m.id) FILTER (
                    WHERE m.target_date IS NOT NULL
                      AND m.target_date < CURRENT_DATE
                      AND m.status NOT IN ('completed', 'cancelled')
                ) AS overdue_milestones,
                COUNT(DISTINCT b.id) FILTER (
                    WHERE b.status NOT IN ('resolved', 'cancelled')
                ) AS open_blockers,
                COUNT(DISTINCT s.id) FILTER (
                    WHERE s.id IS NOT NULL
                      AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
                ) AS open_tasks,
                COUNT(DISTINCT s.id) FILTER (
                    WHERE s.id IS NOT NULL
                      AND s.deadline IS NOT NULL
                      AND s.deadline::date = CURRENT_DATE
                      AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
                ) AS due_today_tasks,
                COUNT(DISTINCT s.id) FILTER (
                    WHERE s.id IS NOT NULL
                      AND s.deadline IS NOT NULL
                      AND s.deadline::date < CURRENT_DATE
                      AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
                ) AS overdue_tasks
            FROM mission_projects p
            LEFT JOIN mission_project_milestones m ON m.project_id = p.id
            LEFT JOIN mission_project_blockers b ON b.project_id = p.id
            LEFT JOIN sag_sager s ON s.project_id = p.id AND s.deleted_at IS NULL
            GROUP BY p.id, p.name, p.description, p.status, p.score, p.started_at, p.ended_at, p.updated_at
            ORDER BY p.updated_at DESC, p.id DESC
            LIMIT %s
            """,
            (limit,),
        ) or []

        result: list[Dict[str, Any]] = []
        for row in rows:
            item = dict(row)
            item.update(MissionService._compute_project_risk(item))
            result.append(item)

        # Important fallback: migration may have created mission_projects but with zero rows.
        if not result:
            return MissionService._get_projects_from_cases(limit)
        return result

    @staticmethod
    def get_project_detail(project_id: int) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_projects"):
            return None

        rows = MissionService.get_projects(limit=200)
        project = next((item for item in rows if int(item.get("id") or 0) == int(project_id)), None)
        if not project:
            return None

        milestones = execute_query(
            """
            SELECT id, project_id, title, description, status, target_date, created_at, updated_at
            FROM mission_project_milestones
            WHERE project_id = %s
            ORDER BY target_date ASC NULLS LAST, id DESC
            """,
            (project_id,),
        ) or []

        blockers = execute_query(
            """
            SELECT id, project_id, title, description, status, severity, resolved_at, created_at, updated_at
            FROM mission_project_blockers
            WHERE project_id = %s
            ORDER BY
                CASE severity
                    WHEN 'critical' THEN 4
                    WHEN 'high' THEN 3
                    WHEN 'medium' THEN 2
                    WHEN 'low' THEN 1
                    ELSE 0
                END DESC,
                id DESC
            """,
            (project_id,),
        ) or []

        tasks = execute_query(
            """
            SELECT
                s.id,
                s.titel,
                s.status,
                s.priority,
                s.deadline,
                s.ansvarlig_bruger_id,
                s.project_milestone_id,
                s.is_project_blocker,
                s.project_task_type,
                s.created_at,
                COALESCE(ts.open_todo_count, 0) AS open_todo_count,
                COALESCE(ts.open_todo_titles, ARRAY[]::text[]) AS open_todo_titles
            FROM sag_sager s
            LEFT JOIN LATERAL (
                SELECT
                    COUNT(*) FILTER (
                        WHERE t.deleted_at IS NULL
                          AND COALESCE(t.is_done, FALSE) = FALSE
                    ) AS open_todo_count,
                    ARRAY_REMOVE(
                        ARRAY_AGG(
                            CASE
                                WHEN t.deleted_at IS NULL
                                  AND COALESCE(t.is_done, FALSE) = FALSE
                                THEN t.title
                            END
                            ORDER BY COALESCE(t.due_date, DATE '9999-12-31') ASC, t.id ASC
                        ),
                        NULL
                    ) AS open_todo_titles
                FROM sag_todo_steps t
                WHERE t.sag_id = s.id
            ) ts ON TRUE
            WHERE s.deleted_at IS NULL
              AND s.project_id = %s
            ORDER BY
                s.deadline ASC NULLS LAST,
                s.created_at DESC
            LIMIT 200
            """,
            (project_id,),
        ) or []

        project_open_todo_count = 0
        project_open_todo_titles: list[str] = []
        if MissionService._table_exists("sag_todo_steps"):
            project_todo_row = execute_query_single(
                """
                SELECT
                    COUNT(*) FILTER (
                        WHERE t.deleted_at IS NULL
                          AND COALESCE(t.is_done, FALSE) = FALSE
                    ) AS open_todo_count,
                    ARRAY_REMOVE(
                        ARRAY_AGG(
                            CASE
                                WHEN t.deleted_at IS NULL
                                  AND COALESCE(t.is_done, FALSE) = FALSE
                                THEN t.title
                            END
                            ORDER BY COALESCE(t.due_date, DATE '9999-12-31') ASC, t.id ASC
                        ),
                        NULL
                    ) AS open_todo_titles
                FROM sag_todo_steps t
                WHERE t.sag_id = %s
                """,
                (project_id,),
            ) or {}
            project_open_todo_count = int(project_todo_row.get("open_todo_count") or 0)
            titles_raw = project_todo_row.get("open_todo_titles") or []
            if isinstance(titles_raw, list):
                project_open_todo_titles = [str(item).strip() for item in titles_raw if str(item or "").strip()]

        # Fallback for case-backed projects: fetch directly related/under cases from relation table.
        # This is used when a project is a case of type project/projekt and tasks are linked as case relations.
        if not tasks and MissionService._table_exists("sag_relationer"):
            tasks = execute_query(
                """
                WITH related AS (
                    SELECT
                        sr.målsag_id AS task_id,
                        sr.relationstype AS relation_type
                    FROM sag_relationer sr
                    WHERE sr.deleted_at IS NULL
                      AND sr.kilde_sag_id = %s

                    UNION ALL

                    SELECT
                        sr.kilde_sag_id AS task_id,
                        sr.relationstype AS relation_type
                    FROM sag_relationer sr
                    WHERE sr.deleted_at IS NULL
                      AND sr.målsag_id = %s
                )
                SELECT
                    s.id,
                    s.titel,
                    s.status,
                    s.priority,
                    s.deadline,
                    s.ansvarlig_bruger_id,
                    s.project_milestone_id,
                    s.is_project_blocker,
                    COALESCE(NULLIF(TRIM(s.project_task_type), ''), r.relation_type) AS project_task_type,
                    s.created_at,
                    COALESCE(ts.open_todo_count, 0) AS open_todo_count,
                    COALESCE(ts.open_todo_titles, ARRAY[]::text[]) AS open_todo_titles
                FROM related r
                JOIN sag_sager s ON s.id = r.task_id
                LEFT JOIN LATERAL (
                    SELECT
                        COUNT(*) FILTER (
                            WHERE t.deleted_at IS NULL
                              AND COALESCE(t.is_done, FALSE) = FALSE
                        ) AS open_todo_count,
                        ARRAY_REMOVE(
                            ARRAY_AGG(
                                CASE
                                    WHEN t.deleted_at IS NULL
                                      AND COALESCE(t.is_done, FALSE) = FALSE
                                    THEN t.title
                                END
                                ORDER BY COALESCE(t.due_date, DATE '9999-12-31') ASC, t.id ASC
                            ),
                            NULL
                        ) AS open_todo_titles
                    FROM sag_todo_steps t
                    WHERE t.sag_id = s.id
                ) ts ON TRUE
                WHERE s.deleted_at IS NULL
                  AND s.id <> %s
                ORDER BY
                    s.deadline ASC NULLS LAST,
                    s.created_at DESC
                LIMIT 200
                """,
                (project_id, project_id, project_id),
            ) or []

        return {
            **project,
            "milestones": [dict(row) for row in milestones],
            "blockers": [dict(row) for row in blockers],
            "tasks": [dict(row) for row in tasks],
            "project_open_todo_count": project_open_todo_count,
            "project_open_todo_titles": project_open_todo_titles,
        }

    @staticmethod
    def get_projects_state_payload(limit: int = 120) -> Dict[str, Any]:
        projects = MissionService.get_projects(limit)
        workload = MissionService.get_project_workload(limit)

        details: Dict[str, Any] = {}
        for project in projects:
            project_id = int(project.get("id") or 0)
            if project_id <= 0:
                continue
            detail = MissionService.get_project_detail(project_id)
            if detail:
                details[str(project_id)] = detail

        total = len(projects)
        active = len([p for p in projects if str(p.get("status") or "").lower() == "active"])
        high_risk = len([p for p in projects if p.get("risk_level") in {"high", "critical"}])
        blocked = len([p for p in projects if int(p.get("open_blockers") or 0) > 0])
        due_today = sum(int(p.get("due_today_tasks") or 0) for p in projects)

        return {
            "summary": {
                "total": total,
                "active": active,
                "high_risk": high_risk,
                "blocked": blocked,
                "due_today": due_today,
            },
            "projects": projects,
            "workload": workload,
            "details": details,
        }

    @staticmethod
    def create_project(payload: Dict[str, Any], actor_user_id: Optional[int] = None) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_projects"):
            return None

        name = str(payload.get("name") or "").strip()
        if not name:
            return None

        description = str(payload.get("description") or "").strip() or None
        status = str(payload.get("status") or "planned").strip().lower()
        if status not in {"planned", "active", "paused", "completed", "cancelled"}:
            status = "planned"

        try:
            score = int(payload.get("score") or 0)
        except (TypeError, ValueError):
            score = 0

        started_at = payload.get("started_at")
        ended_at = payload.get("ended_at")

        rows = execute_query(
            """
            INSERT INTO mission_projects (
                name,
                description,
                status,
                score,
                started_at,
                ended_at,
                created_by,
                payload,
                updated_at
            )
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb, NOW())
            RETURNING id
            """,
            (
                name,
                description,
                status,
                score,
                started_at,
                ended_at,
                actor_user_id,
                json.dumps(payload.get("payload") or {}),
            ),
        ) or []

        if not rows:
            return None
        return MissionService.get_project_detail(int(rows[0].get("id")))

    @staticmethod
    def update_project(project_id: int, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_projects"):
            return None

        updates: list[str] = []
        values: list[Any] = []

        if "name" in payload:
            name = str(payload.get("name") or "").strip()
            if name:
                updates.append("name = %s")
                values.append(name)

        if "description" in payload:
            updates.append("description = %s")
            values.append(str(payload.get("description") or "").strip() or None)

        if "status" in payload:
            status = str(payload.get("status") or "").strip().lower()
            if status in {"planned", "active", "paused", "completed", "cancelled"}:
                updates.append("status = %s")
                values.append(status)

        if "score" in payload:
            try:
                updates.append("score = %s")
                values.append(int(payload.get("score") or 0))
            except (TypeError, ValueError):
                pass

        if "started_at" in payload:
            updates.append("started_at = %s")
            values.append(payload.get("started_at"))

        if "ended_at" in payload:
            updates.append("ended_at = %s")
            values.append(payload.get("ended_at"))

        if not updates:
            return MissionService.get_project_detail(project_id)

        updates.append("updated_at = NOW()")
        values.append(project_id)

        execute_query(
            f"""
            UPDATE mission_projects
            SET {', '.join(updates)}
            WHERE id = %s
            """,
            tuple(values),
        )

        return MissionService.get_project_detail(project_id)

    @staticmethod
    def add_project_milestone(project_id: int, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_project_milestones"):
            return None

        title = str(payload.get("title") or "").strip()
        if not title:
            return None

        description = str(payload.get("description") or "").strip() or None
        status = str(payload.get("status") or "active").strip().lower()
        if status not in {"active", "completed", "cancelled"}:
            status = "active"
        target_date = payload.get("target_date")

        rows = execute_query(
            """
            INSERT INTO mission_project_milestones (project_id, title, description, status, target_date, updated_at)
            VALUES (%s, %s, %s, %s, %s, NOW())
            RETURNING id, project_id, title, description, status, target_date, created_at, updated_at
            """,
            (project_id, title, description, status, target_date),
        ) or []

        return dict(rows[0]) if rows else None

    @staticmethod
    def update_project_milestone(project_id: int, milestone_id: int, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_project_milestones"):
            return None

        updates: list[str] = []
        values: list[Any] = []

        if "title" in payload:
            title = str(payload.get("title") or "").strip()
            if title:
                updates.append("title = %s")
                values.append(title)

        if "description" in payload:
            updates.append("description = %s")
            values.append(str(payload.get("description") or "").strip() or None)

        if "status" in payload:
            status = str(payload.get("status") or "").strip().lower()
            if status in {"active", "completed", "cancelled"}:
                updates.append("status = %s")
                values.append(status)

        if "target_date" in payload:
            updates.append("target_date = %s")
            values.append(payload.get("target_date"))

        if not updates:
            return execute_query_single(
                """
                SELECT id, project_id, title, description, status, target_date, created_at, updated_at
                FROM mission_project_milestones
                WHERE id = %s AND project_id = %s
                """,
                (milestone_id, project_id),
            )

        updates.append("updated_at = NOW()")
        values.extend([milestone_id, project_id])
        rows = execute_query(
            f"""
            UPDATE mission_project_milestones
            SET {', '.join(updates)}
            WHERE id = %s AND project_id = %s
            RETURNING id, project_id, title, description, status, target_date, created_at, updated_at
            """,
            tuple(values),
        ) or []

        return dict(rows[0]) if rows else None

    @staticmethod
    def add_project_blocker(project_id: int, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_project_blockers"):
            return None

        title = str(payload.get("title") or "").strip()
        if not title:
            return None

        description = str(payload.get("description") or "").strip() or None
        status = str(payload.get("status") or "open").strip().lower()
        if status not in {"open", "in_progress", "resolved", "cancelled"}:
            status = "open"

        severity = str(payload.get("severity") or "medium").strip().lower()
        if severity not in {"low", "medium", "high", "critical"}:
            severity = "medium"

        resolved_at = payload.get("resolved_at")
        rows = execute_query(
            """
            INSERT INTO mission_project_blockers (project_id, title, description, status, severity, resolved_at, updated_at)
            VALUES (%s, %s, %s, %s, %s, %s, NOW())
            RETURNING id, project_id, title, description, status, severity, resolved_at, created_at, updated_at
            """,
            (project_id, title, description, status, severity, resolved_at),
        ) or []

        return dict(rows[0]) if rows else None

    @staticmethod
    def update_project_blocker(project_id: int, blocker_id: int, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_project_blockers"):
            return None

        updates: list[str] = []
        values: list[Any] = []

        if "title" in payload:
            title = str(payload.get("title") or "").strip()
            if title:
                updates.append("title = %s")
                values.append(title)

        if "description" in payload:
            updates.append("description = %s")
            values.append(str(payload.get("description") or "").strip() or None)

        if "status" in payload:
            status = str(payload.get("status") or "").strip().lower()
            if status in {"open", "in_progress", "resolved", "cancelled"}:
                updates.append("status = %s")
                values.append(status)
                if status == "resolved" and "resolved_at" not in payload:
                    updates.append("resolved_at = NOW()")

        if "severity" in payload:
            severity = str(payload.get("severity") or "").strip().lower()
            if severity in {"low", "medium", "high", "critical"}:
                updates.append("severity = %s")
                values.append(severity)

        if "resolved_at" in payload:
            updates.append("resolved_at = %s")
            values.append(payload.get("resolved_at"))

        if not updates:
            return execute_query_single(
                """
                SELECT id, project_id, title, description, status, severity, resolved_at, created_at, updated_at
                FROM mission_project_blockers
                WHERE id = %s AND project_id = %s
                """,
                (blocker_id, project_id),
            )

        updates.append("updated_at = NOW()")
        values.extend([blocker_id, project_id])
        rows = execute_query(
            f"""
            UPDATE mission_project_blockers
            SET {', '.join(updates)}
            WHERE id = %s AND project_id = %s
            RETURNING id, project_id, title, description, status, severity, resolved_at, created_at, updated_at
            """,
            tuple(values),
        ) or []

        return dict(rows[0]) if rows else None

    @staticmethod
    def link_case_to_project(project_id: int, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        if not MissionService._table_exists("mission_projects") or not MissionService._table_exists("sag_sager"):
            return None

        sag_id = payload.get("sag_id")
        if sag_id is None:
            return None

        project_exists = execute_query_single("SELECT id FROM mission_projects WHERE id = %s", (project_id,))
        if not project_exists:
            return None

        milestone_id = payload.get("project_milestone_id")
        is_project_blocker = bool(payload.get("is_project_blocker") or False)
        project_task_type = str(payload.get("project_task_type") or "").strip() or None

        rows = execute_query(
            """
            UPDATE sag_sager
            SET
                project_id = %s,
                project_milestone_id = %s,
                is_project_blocker = %s,
                project_task_type = %s
            WHERE id = %s
              AND deleted_at IS NULL
            RETURNING id, titel, status, project_id, project_milestone_id, is_project_blocker, project_task_type
            """,
            (project_id, milestone_id, is_project_blocker, project_task_type, sag_id),
        ) or []

        return dict(rows[0]) if rows else None

    @staticmethod
    def get_assignment_users(limit: int = 300) -> list[Dict[str, Any]]:
        rows = execute_query(
            """
            SELECT
                u.user_id,
                COALESCE(NULLIF(TRIM(u.full_name), ''), NULLIF(TRIM(u.username), ''), CONCAT('Bruger #', u.user_id::text)) AS display_name
            FROM users u
            WHERE COALESCE(u.is_active, TRUE) = TRUE
            ORDER BY display_name ASC
            LIMIT %s
            """,
            (limit,),
        ) or []
        return [
            {
                "user_id": int(row.get("user_id") or 0),
                "display_name": row.get("display_name") or "Ukendt",
            }
            for row in rows
            if row.get("user_id") is not None
        ]

    @staticmethod
    def get_assignment_groups(limit: int = 200) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("groups"):
            return []

        rows = execute_query(
            """
            SELECT id, COALESCE(NULLIF(TRIM(name), ''), CONCAT('Gruppe #', id::text)) AS name
            FROM groups
            ORDER BY name ASC
            LIMIT %s
            """,
            (limit,),
        ) or []
        return [
            {
                "id": int(row.get("id") or 0),
                "name": row.get("name") or "Ukendt gruppe",
            }
            for row in rows
            if row.get("id") is not None
        ]

    @staticmethod
    def get_day_unassigned_cases(limit: int = 120) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("sag_sager"):
            return []

        rows = execute_query(
            """
            SELECT
                s.id,
                s.titel,
                s.beskrivelse,
                s.status,
                s.priority,
                s.start_date,
                s.deadline,
                s.created_at,
                s.ansvarlig_bruger_id,
                s.assigned_group_id,
                COALESCE(NULLIF(TRIM(u.full_name), ''), NULLIF(TRIM(u.username), ''), CONCAT('Bruger #', s.ansvarlig_bruger_id::text)) AS ansvarlig_navn,
                COALESCE(NULLIF(TRIM(g.name), ''), CONCAT('Gruppe #', s.assigned_group_id::text)) AS assigned_group_name,
                COALESCE(c.name, 'Ukendt kunde') AS customer_name
            FROM sag_sager s
            LEFT JOIN customers c ON c.id = s.customer_id
            LEFT JOIN users u ON u.user_id = s.ansvarlig_bruger_id
            LEFT JOIN groups g ON g.id = s.assigned_group_id
            WHERE s.deleted_at IS NULL
              AND LOWER(COALESCE(s.status, '')) NOT IN ('afsluttet', 'lukket', 'closed')
              AND (s.ansvarlig_bruger_id IS NULL OR s.assigned_group_id IS NULL)
            ORDER BY
                CASE
                    WHEN s.ansvarlig_bruger_id IS NULL AND s.assigned_group_id IS NULL THEN 0
                    ELSE 1
                END ASC,
                CASE LOWER(COALESCE(s.priority::text, ''))
                    WHEN 'kritisk' THEN 5
                    WHEN 'critical' THEN 5
                    WHEN 'høj' THEN 4
                    WHEN 'hoj' THEN 4
                    WHEN 'high' THEN 4
                    WHEN 'urgent' THEN 4
                    WHEN 'medium' THEN 3
                    WHEN 'normal' THEN 2
                    WHEN 'lav' THEN 1
                    WHEN 'low' THEN 1
                    ELSE 2
                END DESC,
                s.deadline ASC NULLS LAST,
                s.created_at ASC
            LIMIT %s
            """,
            (limit,),
        ) or []
        return [dict(row) for row in rows]

    @staticmethod
    def get_day_agent_workloads(limit_agents: int = 60, limit_cases_per_agent: int = 20) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("sag_sager"):
            return []

        rows = execute_query(
            """
            WITH active_cases AS (
                SELECT
                    s.id,
                    s.titel,
                    s.status,
                    s.priority,
                    s.start_date,
                    s.deadline,
                    s.created_at,
                    COALESCE(c.name, 'Ukendt kunde') AS customer_name,
                    s.ansvarlig_bruger_id,
                    s.assigned_group_id,
                    COALESCE(NULLIF(TRIM(u.full_name), ''), NULLIF(TRIM(u.username), ''), CONCAT('Bruger #', s.ansvarlig_bruger_id::text)) AS assignee_name,
                    COALESCE(NULLIF(TRIM(g.name), ''), CONCAT('Gruppe #', s.assigned_group_id::text)) AS group_name
                FROM sag_sager s
                LEFT JOIN customers c ON c.id = s.customer_id
                LEFT JOIN users u ON u.user_id = s.ansvarlig_bruger_id
                LEFT JOIN groups g ON g.id = s.assigned_group_id
                WHERE s.deleted_at IS NULL
                  AND LOWER(COALESCE(s.status, '')) <> 'afsluttet'
                  AND (
                      (s.start_date IS NOT NULL AND s.start_date::date <= CURRENT_DATE)
                      OR
                      (s.deadline IS NOT NULL AND s.deadline::date <= CURRENT_DATE)
                  )
            ),
            grouped AS (
                SELECT
                    COALESCE(
                        CASE
                            WHEN ansvarlig_bruger_id IS NOT NULL THEN CONCAT('user:', ansvarlig_bruger_id::text)
                            WHEN assigned_group_id IS NOT NULL THEN CONCAT('group:', assigned_group_id::text)
                            ELSE 'unassigned'
                        END,
                        'unassigned'
                    ) AS assignee_key,
                    COALESCE(assignee_name, group_name, 'Ufordelt') AS assignee_name,
                    COUNT(*) AS total_cases,
                    COUNT(*) FILTER (WHERE deadline IS NOT NULL AND deadline::date < CURRENT_DATE) AS overdue_cases,
                    COUNT(*) FILTER (WHERE deadline IS NOT NULL AND deadline::date = CURRENT_DATE) AS due_today_cases,
                    COUNT(*) FILTER (WHERE start_date IS NOT NULL AND start_date::date <= CURRENT_DATE) AS started_cases,
                    JSONB_AGG(
                        JSONB_BUILD_OBJECT(
                            'id', id,
                            'titel', titel,
                            'status', status,
                            'priority', priority,
                            'customer_name', customer_name,
                            'start_date', start_date,
                            'deadline', deadline,
                            'created_at', created_at
                        )
                        ORDER BY
                            CASE
                                WHEN deadline IS NOT NULL AND deadline::date < CURRENT_DATE THEN 0
                                WHEN deadline IS NOT NULL AND deadline::date = CURRENT_DATE THEN 1
                                ELSE 2
                            END,
                            deadline ASC NULLS LAST,
                            created_at ASC
                    ) AS case_list
                FROM active_cases
                GROUP BY
                    COALESCE(
                        CASE
                            WHEN ansvarlig_bruger_id IS NOT NULL THEN CONCAT('user:', ansvarlig_bruger_id::text)
                            WHEN assigned_group_id IS NOT NULL THEN CONCAT('group:', assigned_group_id::text)
                            ELSE 'unassigned'
                        END,
                        'unassigned'
                    ),
                    COALESCE(assignee_name, group_name, 'Ufordelt')
            )
            SELECT
                assignee_key,
                assignee_name,
                total_cases,
                overdue_cases,
                due_today_cases,
                started_cases,
                CASE
                    WHEN case_list IS NULL THEN '[]'::jsonb
                    ELSE case_list
                END AS case_list
            FROM grouped
            ORDER BY overdue_cases DESC, due_today_cases DESC, total_cases DESC, assignee_name ASC
            LIMIT %s
            """,
            (limit_agents,),
        ) or []

        result: list[Dict[str, Any]] = []
        for row in rows:
            case_list = row.get("case_list")
            if isinstance(case_list, list):
                trimmed_cases = case_list[: max(1, int(limit_cases_per_agent))]
            else:
                trimmed_cases = []

            result.append(
                {
                    "assignee_key": row.get("assignee_key") or "unassigned",
                    "assignee_name": row.get("assignee_name") or "Ufordelt",
                    "total_cases": int(row.get("total_cases") or 0),
                    "overdue_cases": int(row.get("overdue_cases") or 0),
                    "due_today_cases": int(row.get("due_today_cases") or 0),
                    "started_cases": int(row.get("started_cases") or 0),
                    "case_list": trimmed_cases,
                }
            )

        return result

    @staticmethod
    def get_recent_emails(limit: int = 25) -> list[Dict[str, Any]]:
        if not MissionService._table_exists("email_messages"):
            logger.warning("Mission table missing: email_messages (recent emails unavailable)")
            return []

        rows = execute_query(
            """
            SELECT
                id,
                subject,
                sender_name,
                sender_email,
                classification,
                status,
                linked_case_id,
                received_date
            FROM email_messages
            WHERE deleted_at IS NULL
            ORDER BY received_date DESC NULLS LAST, created_at DESC
            LIMIT %s
            """,
            (limit,),
        ) or []

        return [dict(row) for row in rows]

    @staticmethod
    def get_environment_readings(limit: int = 12) -> list[Dict[str, Any]]:
        readings = MissionService.parse_json_setting("mission_environment_readings", [])
        if not isinstance(readings, list):
            return []

        result: list[Dict[str, Any]] = []
        for raw in readings:
            if not isinstance(raw, dict):
                continue
            item = {
                "sensor_id": str(raw.get("sensor_id") or "").strip() or None,
                "sensor_name": str(raw.get("sensor_name") or "").strip() or "Sensor",
                "temperature": raw.get("temperature"),
                "unit": str(raw.get("unit") or "°C").strip() or "°C",
                "timestamp": raw.get("timestamp"),
                "payload": raw.get("payload") if isinstance(raw.get("payload"), dict) else {},
            }
            result.append(item)

        return result[: max(1, int(limit))]

    @staticmethod
    def get_state() -> Dict[str, Any]:
        kpis_default = {
            "open_cases": 0,
            "new_cases": 0,
            "unassigned_cases": 0,
            "deadlines_today": 0,
            "overdue_deadlines": 0,
        }

        return {
            "kpis": MissionService._safe("kpis", MissionService.get_kpis, kpis_default),
            "active_calls": MissionService._safe("active_calls", MissionService.get_active_calls, []),
            "employee_deadlines": MissionService._safe("employee_deadlines", MissionService.get_employee_deadlines, []),
            "active_alerts": MissionService._safe("active_alerts", MissionService.get_active_alerts, []),
            "live_feed": MissionService._safe("live_feed", lambda: MissionService.get_live_feed(20), []),
            "important_cases": MissionService._safe("important_cases", lambda: MissionService.get_important_cases(80), []),
            "projects": MissionService._safe("projects", lambda: MissionService.get_projects_state_payload(120), {"summary": {}, "projects": [], "workload": []}),
            "day_unassigned_cases": MissionService._safe("day_unassigned_cases", lambda: MissionService.get_day_unassigned_cases(120), []),
            "day_agent_workloads": MissionService._safe("day_agent_workloads", lambda: MissionService.get_day_agent_workloads(60, 20), []),
            "assignment_users": MissionService._safe("assignment_users", lambda: MissionService.get_assignment_users(300), []),
            "assignment_groups": MissionService._safe("assignment_groups", lambda: MissionService.get_assignment_groups(200), []),
            "recent_emails": MissionService._safe("recent_emails", lambda: MissionService.get_recent_emails(25), []),
            "environment_readings": MissionService._safe("environment_readings", lambda: MissionService.get_environment_readings(12), []),
            "config": {
                "display_queues": MissionService._safe("config.display_queues", lambda: MissionService.parse_json_setting("mission_display_queues", []), []),
                "sound_enabled": MissionService._safe(
                    "config.sound_enabled",
                    lambda: str(MissionService.get_setting_value("mission_sound_enabled", "true")).lower() == "true",
                    True,
                ),
                "sound_volume": MissionService._safe(
                    "config.sound_volume",
                    lambda: int(MissionService.get_setting_value("mission_sound_volume", "70") or 70),
                    70,
                ),
                "sound_events": MissionService._safe(
                    "config.sound_events",
                    lambda: MissionService.parse_json_setting("mission_sound_events", ["incoming_call", "uptime_down", "critical_event"]),
                    ["incoming_call", "uptime_down", "critical_event"],
                ),
                "kpi_visible": MissionService._safe(
                    "config.kpi_visible",
                    lambda: MissionService.parse_json_setting(
                        "mission_kpi_visible",
                        ["open_cases", "new_cases", "unassigned_cases", "deadlines_today", "overdue_deadlines"],
                    ),
                    ["open_cases", "new_cases", "unassigned_cases", "deadlines_today", "overdue_deadlines"],
                ),
                "customer_filter": MissionService._safe(
                    "config.customer_filter",
                    lambda: MissionService.get_setting_value("mission_customer_filter", "") or "",
                    "",
                ),
                "camera_enabled": MissionService._safe(
                    "config.camera_enabled",
                    lambda: str(MissionService.get_setting_value("mission_camera_enabled", "false")).lower() == "true",
                    False,
                ),
                "camera_name": MissionService._safe(
                    "config.camera_name",
                    lambda: MissionService.get_setting_value("mission_camera_name", "Mission Kamera") or "Mission Kamera",
                    "Mission Kamera",
                ),
                "camera_feed_url": MissionService._safe(
                    "config.camera_feed_url",
                    lambda: MissionService.get_setting_value("mission_camera_feed_url", "") or "",
                    "",
                ),
                "camera_spotlight_seconds": MissionService._safe(
                    "config.camera_spotlight_seconds",
                    lambda: max(5, min(int(MissionService.get_setting_value("mission_camera_spotlight_seconds", "20") or 20), 120)),
                    20,
                ),
            },
        }
