"""
FreeExile Server-Authoritative Meridian Passive Service (Huyết Cốt Ma Đồ).
Enforces topological pathing, point deduction, anti-cheat validation,
stat aggregation, Jewel socketing, and synchronization with Combat Engine & Level Progression.
"""

from __future__ import annotations
import json
import sqlite3
import time
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING

from server.world.meridian_types import (
    JewelItemDef,
    MeridianNodeDef,
    MeridianNodeType,
    MeridianResetResult,
    MeridianSocketResult,
    MeridianStatBonus,
    MeridianUnlockResult,
    MeridianUnsocketResult,
    PlayerMeridianState,
)
from server.world.meridian_catalog import (
    CANONICAL_MERIDIAN_NODES,
    get_canonical_jewel,
    get_meridian_node,
    get_node_neighbors,
)

if TYPE_CHECKING:
    from server.world.combat_engine import CombatActor
    from server.world.level_progression_types import LevelUpEvent
    from server.world.quest_engine import QuestEngine


class MeridianService:
    """Server-authoritative controller for meridian passive trees, jewels, and character attributes."""

    DEFAULT_INITIAL_POINTS: int = 5

    def __init__(
        self,
        db_path: Optional[str] = None,
        quest_engine: Optional[QuestEngine] = None,
    ) -> None:
        self._db_path = db_path
        self._quest_engine = quest_engine
        self._players: Dict[str, PlayerMeridianState] = {}
        if self._db_path:
            self._init_db()

    def _execute_sql(self, query: str, params: Tuple[Any, ...] = ()) -> None:
        if not self._db_path:
            return
        conn = sqlite3.connect(self._db_path)
        try:
            with conn:
                conn.execute(query, params)
        finally:
            conn.close()

    def _fetch_one(self, query: str, params: Tuple[Any, ...] = ()) -> Optional[Tuple[Any, ...]]:
        if not self._db_path:
            return None
        conn = sqlite3.connect(self._db_path)
        try:
            cur = conn.cursor()
            cur.execute(query, params)
            return cur.fetchone()
        finally:
            conn.close()

    def _init_db(self) -> None:
        self._execute_sql("""
            CREATE TABLE IF NOT EXISTS character_meridians (
                player_id TEXT PRIMARY KEY,
                available_points INTEGER NOT NULL DEFAULT 5,
                spent_points INTEGER NOT NULL DEFAULT 0,
                unlocked_nodes_json TEXT NOT NULL DEFAULT '[]',
                socketed_jewels_json TEXT NOT NULL DEFAULT '{}',
                updated_at REAL NOT NULL
            );
        """)
        try:
            self._execute_sql("ALTER TABLE character_meridians ADD COLUMN socketed_jewels_json TEXT NOT NULL DEFAULT '{}';")
        except Exception:
            pass

    def get_or_create_player(self, player_id: str) -> PlayerMeridianState:
        """Retrieves in-memory or persists state with authoritative 5 initial points."""
        if player_id in self._players:
            return self._players[player_id]

        if self._db_path:
            try:
                row = self._fetch_one(
                    "SELECT available_points, spent_points, unlocked_nodes_json, socketed_jewels_json FROM character_meridians WHERE player_id = ?",
                    (player_id,),
                )
                if row:
                    unlocked = set(json.loads(row[2]))
                    raw_jewels = json.loads(row[3]) if len(row) > 3 and row[3] else {}
                    socketed: Dict[str, JewelItemDef] = {}
                    for s_id, j_id in raw_jewels.items():
                        j_def = get_canonical_jewel(j_id)
                        if j_def:
                            socketed[s_id] = j_def

                    state = PlayerMeridianState(
                        player_id=player_id,
                        available_points=row[0],
                        spent_points=row[1],
                        unlocked_nodes=unlocked,
                        socketed_jewels=socketed,
                        last_updated_at=time.time(),
                    )
                    self._players[player_id] = state
                    return state
            except Exception:
                pass

        state = PlayerMeridianState(
            player_id=player_id,
            available_points=self.DEFAULT_INITIAL_POINTS,
            spent_points=0,
            unlocked_nodes=set(),
            socketed_jewels={},
            last_updated_at=time.time(),
        )
        self._players[player_id] = state
        self._save_player(state)
        return state

    def _save_player(self, state: PlayerMeridianState) -> None:
        if not self._db_path:
            return
        try:
            jewels_map = {s_id: j.jewel_id for s_id, j in state.socketed_jewels.items()}
            self._execute_sql(
                """
                INSERT INTO character_meridians (player_id, available_points, spent_points, unlocked_nodes_json, socketed_jewels_json, updated_at)
                VALUES (?, ?, ?, ?, ?, ?)
                ON CONFLICT(player_id) DO UPDATE SET
                    available_points = excluded.available_points,
                    spent_points = excluded.spent_points,
                    unlocked_nodes_json = excluded.unlocked_nodes_json,
                    socketed_jewels_json = excluded.socketed_jewels_json,
                    updated_at = excluded.updated_at
                """,
                (
                    state.player_id,
                    state.available_points,
                    state.spent_points,
                    json.dumps(sorted(list(state.unlocked_nodes))),
                    json.dumps(jewels_map),
                    state.last_updated_at,
                ),
            )
        except Exception:
            pass

    def can_unlock_node(self, player_id: str, node_id: str) -> Tuple[bool, str]:
        """Validates topological pathing and point sufficiency."""
        node = get_meridian_node(node_id)
        if not node:
            return False, f"Huyệt vị '{node_id}' không tồn tại trên Ma Đồ."

        state = self.get_or_create_player(player_id)
        if node_id in state.unlocked_nodes:
            return False, f"Huyệt vị {node.name} đã được khai thông từ trước."

        if state.available_points <= 0:
            return False, f"Không đủ Điểm Ma Đồ (Hiện có: {state.available_points}, Cần: 1)."

        # Root origin node is always open for unsealing first
        if node.node_type == MeridianNodeType.ORIGIN or node_id in ("m_c1", "m1"):
            return True, "Hợp lệ để khai thông."

        # Must be connected to at least one already unlocked adjacent node
        neighbors = get_node_neighbors(node_id)
        connected_to_unlocked = any(n in state.unlocked_nodes for n in neighbors)
        if not connected_to_unlocked:
            return False, f"Chưa thể khai thông {node.name}. Bắt buộc phải đả thông ít nhất 1 huyệt kế cận trước."

        return True, "Hợp lệ để khai thông."

    def unlock_node(self, player_id: str, node_id: str) -> MeridianUnlockResult:
        """Authoritatively spends 1 point and unlocks the designated acupoint."""
        eligible, reason = self.can_unlock_node(player_id, node_id)
        state = self.get_or_create_player(player_id)
        node = get_meridian_node(node_id)

        if not eligible or not node:
            return MeridianUnlockResult(
                success=False,
                node_id=node_id,
                message=reason,
                available_points=state.available_points,
                unlocked_nodes=tuple(sorted(state.unlocked_nodes)),
                aggregated_stats=self.compute_total_stats(player_id),
            )

        state.available_points -= 1
        state.spent_points += 1
        state.unlocked_nodes.add(node_id)
        state.last_updated_at = time.time()
        self._save_player(state)

        if self._quest_engine:
            try:
                self._quest_engine.record_meridian_unlock(player_id, node_id)
            except Exception:
                pass

        total_stats = self.compute_total_stats(player_id)
        return MeridianUnlockResult(
            success=True,
            node_id=node_id,
            message=f"Khai thông thành công {node.name} ({node.title})!",
            available_points=state.available_points,
            unlocked_nodes=tuple(sorted(state.unlocked_nodes)),
            aggregated_stats=total_stats,
        )

    def socket_jewel(self, player_id: str, socket_id: str, jewel_id: str) -> MeridianSocketResult:
        """Sockets a Linh Thạch (Jewel) into an unlocked Jewel Socket node."""
        node = get_meridian_node(socket_id)
        if not node or node.node_type != MeridianNodeType.JEWEL_SOCKET:
            return MeridianSocketResult(
                success=False,
                socket_id=socket_id,
                jewel_id=jewel_id,
                message=f"Huyệt vị '{socket_id}' không phải là Linh Cốt Khảm Tọa.",
                aggregated_stats=self.compute_total_stats(player_id),
            )

        state = self.get_or_create_player(player_id)
        if socket_id not in state.unlocked_nodes:
            return MeridianSocketResult(
                success=False,
                socket_id=socket_id,
                jewel_id=jewel_id,
                message=f"Khảm tọa {node.name} chưa được khai thông trên Ma Đồ.",
                aggregated_stats=self.compute_total_stats(player_id),
            )

        jewel = get_canonical_jewel(jewel_id)
        if not jewel:
            return MeridianSocketResult(
                success=False,
                socket_id=socket_id,
                jewel_id=jewel_id,
                message=f"Linh Thạch '{jewel_id}' không tồn tại.",
                aggregated_stats=self.compute_total_stats(player_id),
            )

        state.socketed_jewels[socket_id] = jewel
        state.last_updated_at = time.time()
        self._save_player(state)

        return MeridianSocketResult(
            success=True,
            socket_id=socket_id,
            jewel_id=jewel_id,
            message=f"Khảm nạp thành công {jewel.name} vào {node.name}!",
            aggregated_stats=self.compute_total_stats(player_id),
        )

    def unsocket_jewel(self, player_id: str, socket_id: str) -> MeridianUnsocketResult:
        """Removes a Linh Thạch (Jewel) from an acupoint socket."""
        state = self.get_or_create_player(player_id)
        if socket_id not in state.socketed_jewels:
            return MeridianUnsocketResult(
                success=False,
                socket_id=socket_id,
                removed_jewel_id="",
                message="Khảm tọa này chưa được cắm Linh Thạch nào.",
                aggregated_stats=self.compute_total_stats(player_id),
            )

        removed = state.socketed_jewels.pop(socket_id)
        state.last_updated_at = time.time()
        self._save_player(state)

        return MeridianUnsocketResult(
            success=True,
            socket_id=socket_id,
            removed_jewel_id=removed.jewel_id,
            message=f"Đã tháo gỡ {removed.name} khỏi khảm tọa!",
            aggregated_stats=self.compute_total_stats(player_id),
        )

    def reset_meridian(self, player_id: str) -> MeridianResetResult:
        """Refunds 100% of spent points, unsockets all jewels, and reverts the tree."""
        state = self.get_or_create_player(player_id)
        refunded = state.spent_points
        state.available_points += refunded
        state.spent_points = 0
        state.unlocked_nodes.clear()
        state.socketed_jewels.clear()
        state.last_updated_at = time.time()
        self._save_player(state)

        return MeridianResetResult(
            success=True,
            refunded_points=refunded,
            available_points=state.available_points,
            message=f"Tẩy Tủy hoàn điểm thành công! Hoàn trả {refunded} Điểm Ma Đồ và tháo toàn bộ Linh Thạch.",
        )

    def add_points(self, player_id: str, delta: int) -> int:
        """Authoritatively increments available meridian points (level up / quest reward)."""
        if delta <= 0:
            return self.get_or_create_player(player_id).available_points
        state = self.get_or_create_player(player_id)
        state.available_points += delta
        state.last_updated_at = time.time()
        self._save_player(state)
        return state.available_points

    def on_level_up(self, event: LevelUpEvent) -> None:
        """Handler for level progression service events granting 1 point per level."""
        levels_gained = max(0, event.new_level - event.old_level)
        if levels_gained > 0:
            self.add_points(event.player_id, levels_gained)

    def compute_total_stats(self, player_id: str) -> MeridianStatBonus:
        """Computes aggregated attribute bonuses from all unlocked acupoints and socketed jewels."""
        state = self.get_or_create_player(player_id)
        accumulated = MeridianStatBonus()
        for node_id in state.unlocked_nodes:
            node = get_meridian_node(node_id)
            if node:
                accumulated = accumulated.combine(node.stats)
        for jewel in state.socketed_jewels.values():
            accumulated = accumulated.combine(jewel.stats)
        return accumulated

    def apply_to_combat_actor(self, player_id: str, actor: CombatActor) -> None:
        """Injects authoritative meridian attributes directly into server CombatActor."""
        stats = self.compute_total_stats(player_id)
        old_max_hp = actor.max_hp
        actor.max_hp = old_max_hp + stats.hp
        if actor.current_hp > 0:
            actor.current_hp = min(actor.max_hp, actor.current_hp + stats.hp)

        actor.base_attack = round((actor.base_attack + stats.dps) * (1.0 + stats.dps_mult), 2)
        actor.crit_chance = min(1.0, round(actor.crit_chance + stats.crit_rate, 4))
        actor.crit_multiplier = round(actor.crit_multiplier + stats.crit_dmg, 4)

        if hasattr(actor, "resistances") and isinstance(actor.resistances, dict):
            for elem in list(actor.resistances.keys()):
                actor.resistances[elem] = round(actor.resistances[elem] + stats.resist, 2)
