"""
Game Design Matrix Service for FreeExile.
Central Relational Truth Engine managing cross-cutting dependencies,
integrity validation, DAG topological cycle checks, and analytical queries.
"""

from __future__ import annotations
import os
import sqlite3
import sys
from contextlib import contextmanager
from typing import Any, Dict, List, Optional, Set, Tuple

from server.world.game_design_matrix_types import (
    StoryActRow,
    ZoneRow,
    NpcRow,
    QuestRow,
    ProgressionBenchmarkRow,
    DesignIntegrityReport,
)
from server.world.game_design_matrix_schema import (
    SQL_PRAGMA_SETUP,
    SQL_CREATE_TABLES,
    SQL_CREATE_VIEWS,
)
from server.world.game_design_matrix_seeder import seed_all_canonical_data
from dataclasses import dataclass


@dataclass(slots=True, frozen=True)
class PartyScalingRow:
    party_size: int
    hp_multiplier: float
    exp_bonus_pct: float
    item_quantity_bonus_pct: float
    currency_quantity_bonus_pct: float
    rarity_bonus_pct: float
    nearby_radius: float
    description: str

@dataclass(slots=True, frozen=True)
class LootAllocationRow:
    mode_id: str
    name: str
    timeout_seconds: float
    is_permanent: bool
    description: str

@dataclass(slots=True, frozen=True)
class TeleportConfigRow:
    action_type: str
    cast_time_seconds: float
    cooldown_seconds: float
    cancel_on_movement: bool
    cancel_on_damage: bool
    description: str

@dataclass(slots=True, frozen=True)
class PortalQuarantineRow:
    rule_id: str
    zone_type: str
    boss_lockout: bool
    secret_chamber_lockout: bool
    max_portals: int
    description: str


def _row_to_party_scaling(r: sqlite3.Row) -> PartyScalingRow:
    return PartyScalingRow(r["party_size"], r["hp_multiplier"], r["exp_bonus_pct"], r["item_quantity_bonus_pct"], r["currency_quantity_bonus_pct"], r["rarity_bonus_pct"], r["nearby_radius"], r["description"])


def _row_to_loot_allocation(r: sqlite3.Row) -> LootAllocationRow:
    return LootAllocationRow(r["mode_id"], r["name"], r["timeout_seconds"], bool(r["is_permanent"]), r["description"])


def _row_to_teleport_config(r: sqlite3.Row) -> TeleportConfigRow:
    return TeleportConfigRow(r["action_type"], r["cast_time_seconds"], r["cooldown_seconds"], bool(r["cancel_on_movement"]), bool(r["cancel_on_damage"]), r["description"])


def _row_to_portal_quarantine(r: sqlite3.Row) -> PortalQuarantineRow:
    return PortalQuarantineRow(r["rule_id"], r["zone_type"], bool(r["boss_lockout"]), bool(r["secret_chamber_lockout"]), r["max_portals"], r["description"])


class GameDesignMatrixService:
    """Authoritative Service for querying and validating game design cross-relationships."""

    def __init__(self, db_path: Optional[str] = None):
        if not db_path:
            base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "data"))
            os.makedirs(base_dir, exist_ok=True)
            db_path = os.path.join(base_dir, "game_design_matrix.db")
        self.db_path = db_path
        self._memory_conn: Optional[sqlite3.Connection] = None
        if self.db_path == ":memory:":
            self._memory_conn = sqlite3.connect(":memory:")
            self._memory_conn.row_factory = sqlite3.Row
            self._init_database(self._memory_conn)
        else:
            with self._get_connection() as conn:
                self._init_database(conn)

    @contextmanager
    def _get_connection(self):
        if self._memory_conn is not None:
            yield self._memory_conn
            self._memory_conn.commit()
        else:
            conn = sqlite3.connect(self.db_path)
            conn.row_factory = sqlite3.Row
            conn.executescript(SQL_PRAGMA_SETUP)
            try:
                yield conn
                conn.commit()
            except Exception:
                conn.rollback()
                raise
            finally:
                conn.close()

    def close(self) -> None:
        if self._memory_conn is not None:
            self._memory_conn.close()
            self._memory_conn = None

    def _init_database(self, conn: sqlite3.Connection) -> None:
        """Tạo bảng và view chuẩn trong SQLite."""
        conn.executescript(SQL_PRAGMA_SETUP)
        self._check_and_migrate_schema(conn)
        conn.executescript(SQL_CREATE_TABLES)
        conn.executescript(SQL_CREATE_VIEWS)

    def _check_and_migrate_schema(self, conn: sqlite3.Connection) -> None:
        """Kiểm tra và nâng cấp bảng progression_benchmarks nếu phát hiện schema cũ < 13 cột."""
        cur = conn.cursor()
        cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='progression_benchmarks'")
        if not cur.fetchone():
            return
        cur.execute("PRAGMA table_info(progression_benchmarks)")
        if len(cur.fetchall()) < 13:
            cur.execute("DROP TABLE IF EXISTS progression_benchmarks")

    def seed_canonical_data(self, force: bool = False) -> Dict[str, Any]:
        """Nạp dữ liệu hạt giống chuẩn vào matrix database."""
        with self._get_connection() as conn:
            cur = conn.cursor()
            if not force:
                s_cnt = (cur.execute("SELECT COUNT(*) FROM story_acts").fetchone() or [0])[0]
                b_cnt = (cur.execute("SELECT COUNT(*) FROM progression_benchmarks").fetchone() or [0])[0]
                p_cnt = (cur.execute("SELECT COUNT(*) FROM party_scaling_matrix").fetchone() or [0])[0]
                if s_cnt > 0 and b_cnt >= 100 and p_cnt >= 6:
                    return {"status": "already_seeded"}
            return seed_all_canonical_data(conn)

    # -----------------------------------------------------------------------
    # Analytical Cross-Cutting Query Methods
    # -----------------------------------------------------------------------
    def get_act_narrative_context(self, act_id: str) -> Optional[Dict[str, Any]]:
        """Lấy toàn bộ bối cảnh cốt truyện, vùng đất và boss của một Hồi."""
        with self._get_connection() as conn:
            cur = conn.cursor()
            act_row = cur.execute("SELECT * FROM story_acts WHERE act_id = ?", (act_id,)).fetchone()
            if not act_row:
                return None
            return {
                "act": dict(act_row),
                "zones": [dict(r) for r in cur.execute("SELECT * FROM zones WHERE act_id = ?", (act_id,)).fetchall()],
                "quests": [dict(r) for r in cur.execute("SELECT quest_id, title, required_level FROM quests WHERE act_id = ?", (act_id,)).fetchall()],
            }

    def get_zone_ecosystem(self, zone_id: str) -> Optional[Dict[str, Any]]:
        """Lấy hệ sinh thái toàn diện của một Zone (NPC, Quest, Boss)."""
        with self._get_connection() as conn:
            cur = conn.cursor()
            zone_row = cur.execute("SELECT * FROM zones WHERE zone_id = ?", (zone_id,)).fetchone()
            if not zone_row:
                return None
            return {
                "zone": dict(zone_row),
                "npcs": [dict(r) for r in cur.execute("SELECT * FROM npcs WHERE zone_id = ?", (zone_id,)).fetchall()],
                "quests": [dict(r) for r in cur.execute("SELECT * FROM quests WHERE zone_id = ?", (zone_id,)).fetchall()],
            }

    def get_quest_context(self, quest_id: str) -> Optional[Dict[str, Any]]:
        """Lấy thông tin đa chiều của một Quest từ View hệ sinh thái."""
        with self._get_connection() as conn:
            cur = conn.cursor()
            cur.execute("SELECT * FROM v_quest_ecosystem WHERE quest_id = ?", (quest_id,))
            row = cur.fetchone()
            return dict(row) if row else None

    def get_quest_dependency_chain(self, quest_id: str) -> List[str]:
        """Dò ngược toàn bộ chuỗi nhiệm vụ tiền nhiệm (Prerequisite Chain)."""
        chain: List[str] = []
        visited: Set[str] = set()
        curr = quest_id

        with self._get_connection() as conn:
            cur = conn.cursor()
            while curr and curr not in visited:
                visited.add(curr)
                cur.execute("SELECT prerequisite_quest_id FROM quests WHERE quest_id = ?", (curr,))
                row = cur.fetchone()
                if row and row["prerequisite_quest_id"]:
                    prereq = row["prerequisite_quest_id"]
                    chain.append(prereq)
                    curr = prereq
                else:
                    break
        return chain

    def get_level_progression_benchmark(self, level: int) -> Optional[ProgressionBenchmarkRow]:
        """Truy xuất mốc chuẩn toán học của một cấp độ cụ thể."""
        with self._get_connection() as conn:
            cur = conn.cursor()
            cur.execute("SELECT * FROM progression_benchmarks WHERE level = ?", (level,))
            row = cur.fetchone()
            if not row:
                return None
            k = row.keys()
            return ProgressionBenchmarkRow(
                level=row["level"], target_exp=row["target_exp"],
                exp_to_next_level=row["exp_to_next_level"] if "exp_to_next_level" in k else 0,
                cumulative_exp=row["cumulative_exp"] if "cumulative_exp" in k else row["target_exp"],
                player_base_hp=row["player_base_hp"], player_benchmark_dps=row["player_benchmark_dps"],
                monster_base_hp=row["monster_base_hp"], monster_base_dps=row["monster_base_dps"],
                max_affix_tier_allowed=row["max_affix_tier_allowed"],
                death_penalty_ratio=row["death_penalty_ratio"] if "death_penalty_ratio" in k else 0.0,
                level_gap_safe_range=row["level_gap_safe_range"] if "level_gap_safe_range" in k else 5,
                level_gap_penalty_exp=row["level_gap_penalty_exp"] if "level_gap_penalty_exp" in k else 0.60,
                monster_benchmark_exp=row["monster_benchmark_exp"] if "monster_benchmark_exp" in k else 25,
            )

    def query_cross_relations(self, source_type: Optional[str] = None, source_id: Optional[str] = None) -> List[Dict[str, Any]]:
        """Truy vấn đồ thị quan hệ chéo đa chiều."""
        with self._get_connection() as conn:
            cur = conn.cursor()
            if source_type and source_id:
                cur.execute("SELECT * FROM cross_relationships WHERE source_type = ? AND source_id = ?", (source_type, source_id))
            elif source_type:
                cur.execute("SELECT * FROM cross_relationships WHERE source_type = ?", (source_type,))
            else:
                cur.execute("SELECT * FROM cross_relationships")
            return [dict(r) for r in cur.fetchall()]

    # -----------------------------------------------------------------------
    # Party, Social, Teleport & Quarantine Matrix Queries
    # -----------------------------------------------------------------------
    def get_party_scaling(self, party_size: int) -> Optional[PartyScalingRow]:
        """Truy xuất cấu hình tỷ lệ máu quái, exp, rơi đồ theo số lượng thành viên party (1..6)."""
        with self._get_connection() as conn:
            r = conn.cursor().execute("SELECT * FROM party_scaling_matrix WHERE party_size = ?", (party_size,)).fetchone()
            return _row_to_party_scaling(r) if r else None

    def get_all_party_scalings(self) -> List[PartyScalingRow]:
        """Truy xuất toàn bộ danh sách scaling từ 1 đến 6 người chơi."""
        with self._get_connection() as conn:
            return [_row_to_party_scaling(r) for r in conn.cursor().execute("SELECT * FROM party_scaling_matrix ORDER BY party_size ASC")]

    def get_loot_allocation_mode(self, mode_id: str) -> Optional[LootAllocationRow]:
        """Lấy thông tin cấu hình chế độ phân bổ vật phẩm rơi (PERMANENT, SHORT, FREE_FOR_ALL)."""
        with self._get_connection() as conn:
            r = conn.cursor().execute("SELECT * FROM loot_allocation_modes WHERE mode_id = ?", (mode_id.upper(),)).fetchone()
            return _row_to_loot_allocation(r) if r else None

    def get_all_loot_allocation_modes(self) -> List[LootAllocationRow]:
        """Lấy toàn bộ các chế độ phân bổ vật phẩm."""
        with self._get_connection() as conn:
            return [_row_to_loot_allocation(r) for r in conn.cursor().execute("SELECT * FROM loot_allocation_modes")]

    def get_teleport_config(self, action_type: str = "PARTY_MEMBER") -> Optional[TeleportConfigRow]:
        """Lấy cấu hình thời gian niệm, hồi chiêu và điều kiện hủy của dịch chuyển tức thời."""
        with self._get_connection() as conn:
            r = conn.cursor().execute("SELECT * FROM teleport_configs WHERE action_type = ?", (action_type.upper(),)).fetchone()
            return _row_to_teleport_config(r) if r else None

    def get_all_teleport_configs(self) -> List[TeleportConfigRow]:
        """Lấy toàn bộ cấu hình dịch chuyển tức thời."""
        with self._get_connection() as conn:
            return [_row_to_teleport_config(r) for r in conn.cursor().execute("SELECT * FROM teleport_configs")]

    def get_portal_quarantine_rule(self, zone_type: str = "WILDERNESS") -> Optional[PortalQuarantineRow]:
        """Lấy quy tắc cách ly và khóa cổng theo loại zone (WILDERNESS, DUNGEON, SECRET_CHAMBER, SAFE_HAVEN)."""
        with self._get_connection() as conn:
            r = conn.cursor().execute("SELECT * FROM portal_quarantine_rules WHERE zone_type = ?", (zone_type.upper(),)).fetchone()
            return _row_to_portal_quarantine(r) if r else None

    def get_all_portal_quarantine_rules(self) -> List[PortalQuarantineRow]:
        """Lấy toàn bộ quy tắc cách ly cổng."""
        with self._get_connection() as conn:
            return [_row_to_portal_quarantine(r) for r in conn.cursor().execute("SELECT * FROM portal_quarantine_rules")]

    # -----------------------------------------------------------------------
    # Integrity Verification & Anti-Drift Engine
    # -----------------------------------------------------------------------
    def validate_game_design_integrity(self) -> DesignIntegrityReport:
        """Kiểm tra toàn diện 100% tính toàn vẹn quan hệ chéo và chống Drift."""
        violations: List[str] = []
        warnings: List[str] = []

        with self._get_connection() as conn:
            cur = conn.cursor()

            # 1. Counts
            counts = {}
            for tbl in [
                "story_acts", "zones", "npcs", "quests", "monster_archetypes",
                "skills", "weapon_bases", "affix_tiers", "cross_relationships",
                "party_scaling_matrix", "loot_allocation_modes", "teleport_configs", "portal_quarantine_rules"
            ]:
                cur.execute(f"SELECT COUNT(*) as cnt FROM {tbl}")
                counts[tbl] = cur.fetchone()["cnt"]

            # 2. Check Quest DAG Cycles & Broken Prerequisites
            cur.execute("SELECT quest_id, prerequisite_quest_id, required_level, act_id FROM quests")
            quest_rows = cur.fetchall()
            quest_ids = {r["quest_id"] for r in quest_rows}
            adj: Dict[str, List[str]] = {}

            for r in quest_rows:
                qid = r["quest_id"]
                prereq = r["prerequisite_quest_id"]
                if prereq:
                    if prereq not in quest_ids:
                        violations.append(f"Broken Quest Prerequisite: {qid} references missing quest '{prereq}'.")
                    adj.setdefault(prereq, []).append(qid)

            # Cycle detection via DFS
            has_cycle, cycle_node = _detect_dag_cycle(quest_ids, adj)
            if has_cycle:
                violations.append(f"Circular Dependency in Quest DAG involving quest '{cycle_node}'.")

            # 3. Check Quest Level vs Act Level Bounds
            cur.execute("""
                SELECT q.quest_id, q.required_level, a.act_id, a.level_min, a.level_max 
                FROM quests q 
                JOIN story_acts a ON q.act_id = a.act_id
            """)
            for r in cur.fetchall():
                if r["required_level"] < r["level_min"] - 5 or r["required_level"] > r["level_max"] + 5:
                    warnings.append(
                        f"Pacing Mismatch: Quest '{r['quest_id']}' requires lvl {r['required_level']}, "
                        f"outside Act '{r['act_id']}' range [{r['level_min']}, {r['level_max']}]."
                    )

            # 4. Check Progression Monotonicity
            cur.execute("SELECT * FROM progression_benchmarks ORDER BY level ASC")
            prev_xp = prev_cum = -1
            for r in cur.fetchall():
                if r["target_exp"] <= prev_xp:
                    violations.append(f"Progression Curve non-monotonic at level {r['level']}: XP {r['target_exp']} <= {prev_xp}.")
                prev_xp = r["target_exp"]
                if "cumulative_exp" in r.keys():
                    if r["cumulative_exp"] <= prev_cum:
                        violations.append(f"Progression Cumulative XP non-monotonic at level {r['level']}: XP {r['cumulative_exp']} <= {prev_cum}.")
                    prev_cum = r["cumulative_exp"]

            # 5. Check Party Scaling Matrix (1..6, monotonic scaling, nearby radius 15.0)
            cur.execute("SELECT * FROM party_scaling_matrix ORDER BY party_size ASC")
            party_rows = cur.fetchall()
            if len(party_rows) != 6:
                violations.append(f"Party Scaling: expected 6 sizes (1..6), found {len(party_rows)}.")
            p_hp = p_exp = p_qty = p_curr = p_rar = -1.0
            for pr in party_rows:
                if pr["hp_multiplier"] < p_hp:
                    violations.append(f"Party Scaling HP non-monotonic at size {pr['party_size']}: {pr['hp_multiplier']} < {p_hp}")
                if pr["exp_bonus_pct"] < p_exp:
                    violations.append(f"Party Scaling EXP non-monotonic at size {pr['party_size']}: {pr['exp_bonus_pct']} < {p_exp}")
                if pr["item_quantity_bonus_pct"] < p_qty:
                    violations.append(f"Party Scaling Item Qty non-monotonic at size {pr['party_size']}: {pr['item_quantity_bonus_pct']} < {p_qty}")
                if pr["currency_quantity_bonus_pct"] < p_curr:
                    violations.append(f"Party Scaling Currency Qty non-monotonic at size {pr['party_size']}: {pr['currency_quantity_bonus_pct']} < {p_curr}")
                if pr["rarity_bonus_pct"] < p_rar:
                    violations.append(f"Party Scaling Rarity non-monotonic at size {pr['party_size']}: {pr['rarity_bonus_pct']} < {p_rar}")
                if pr["nearby_radius"] != 15.0:
                    violations.append(f"Party Scaling nearby_radius expected 15.0, found {pr['nearby_radius']}")
                p_hp, p_exp, p_qty = pr["hp_multiplier"], pr["exp_bonus_pct"], pr["item_quantity_bonus_pct"]
                p_curr, p_rar = pr["currency_quantity_bonus_pct"], pr["rarity_bonus_pct"]

            # 6. Check Loot Allocation Modes
            cur.execute("SELECT mode_id, timeout_seconds FROM loot_allocation_modes")
            lmodes = {r["mode_id"]: r["timeout_seconds"] for r in cur.fetchall()}
            for req_m in ("PERMANENT", "SHORT", "FREE_FOR_ALL"):
                if req_m not in lmodes:
                    violations.append(f"Loot Allocation: missing mode '{req_m}'.")
            if lmodes.get("SHORT", 0.0) != 5.0:
                violations.append(f"Loot Allocation SHORT timeout expected 5.0s, found {lmodes.get('SHORT')}s.")

            # 7. Check Teleport Configs & Portal Quarantine Rules
            cur.execute("SELECT action_type, cast_time_seconds, cancel_on_movement, cancel_on_damage FROM teleport_configs")
            for tc in cur.fetchall():
                if tc["cast_time_seconds"] != 3.5 or not tc["cancel_on_movement"] or not tc["cancel_on_damage"]:
                    violations.append(f"Teleport Config '{tc['action_type']}' invalid (cast={tc['cast_time_seconds']}, cancel_move={tc['cancel_on_movement']})")

            cur.execute("SELECT zone_type, boss_lockout, secret_chamber_lockout, max_portals FROM portal_quarantine_rules WHERE zone_type IN ('WILDERNESS', 'DUNGEON')")
            for qr in cur.fetchall():
                if not qr["boss_lockout"] or not qr["secret_chamber_lockout"] or qr["max_portals"] != 6:
                    violations.append(f"Quarantine rule for '{qr['zone_type']}' invalid: boss_lock={qr['boss_lockout']}, max={qr['max_portals']}")

            is_valid = len(violations) == 0

            return DesignIntegrityReport(
                is_valid=is_valid,
                total_acts=counts["story_acts"],
                total_zones=counts["zones"],
                total_npcs=counts["npcs"],
                total_quests=counts["quests"],
                total_monsters=counts["monster_archetypes"],
                total_skills=counts["skills"],
                total_weapons=counts["weapon_bases"],
                total_affixes=counts["affix_tiers"],
                total_relations=counts["cross_relationships"],
                violations=tuple(violations),
                warnings=tuple(warnings),
            )

    def verify_matrix_integrity(self) -> DesignIntegrityReport:
        """Alias for validate_game_design_integrity providing uniform API naming."""
        return self.validate_game_design_integrity()


def _detect_dag_cycle(all_nodes: Set[str], adj: Dict[str, List[str]]) -> Tuple[bool, Optional[str]]:
    """DFS-based cycle detection returning (has_cycle, cycle_root)."""
    visited: Dict[str, int] = {node: 0 for node in all_nodes}  # 0=unvisited, 1=visiting, 2=visited

    def dfs(u: str) -> Optional[str]:
        visited[u] = 1
        for v in adj.get(u, []):
            if visited.get(v, 0) == 1:
                return v
            if visited.get(v, 0) == 0:
                cycle_found = dfs(v)
                if cycle_found:
                    return cycle_found
        visited[u] = 2
        return None

    for node in all_nodes:
        if visited[node] == 0:
            cycle = dfs(node)
            if cycle:
                return True, cycle
    return False, None


if __name__ == "__main__":
    action = sys.argv[1] if len(sys.argv) > 1 else "validate"
    service = GameDesignMatrixService()
    if action == "seed":
        print(f"[GameDesignMatrix] Seeded: {service.seed_canonical_data(force=True)}")
    else:
        report = service.validate_game_design_integrity()
        print(f"[GameDesignMatrix] Integrity: {'PASS' if report.is_valid else 'FAIL'}")
        if report.violations:
            for v in report.violations:
                print(f" - {v}")
            sys.exit(1)
    service.close()
