"""
Quest and Milestone Authoritative Engine for FreeExile.
Implements:
1. Normal Quests (Main and Side Quests).
2. Hidden Quests (Triggered by cryptic environmental & combat criteria).
3. High-Tier / Epic Quests (Endgame Bosses, Keystones, Astral Challenges).
4. Milestone Progression and Tiered Rewards with Anti-Double Claim Protection.
5. Zero-Trust Server Authority validation.
"""

from __future__ import annotations
import time
from typing import Dict, List, Optional, Set, Tuple, Any

from server.world.quest_types import (
    QuestType,
    QuestStatus,
    ObjectiveType,
    HiddenTriggerType,
    RewardItem,
    QuestReward,
    ObjectiveState,
    HiddenTrigger,
    QuestDefinition,
    PlayerQuestProgress,
    MilestoneTier,
)
from server.world.quest_catalog import register_canonical_quests_and_milestones

__all__ = [
    "QuestEngine",
    "create_default_quest_engine",
    "QuestType",
    "QuestStatus",
    "ObjectiveType",
    "HiddenTriggerType",
    "RewardItem",
    "QuestReward",
    "ObjectiveState",
    "HiddenTrigger",
    "QuestDefinition",
    "PlayerQuestProgress",
    "MilestoneTier",
]


class QuestEngine:
    def __init__(self) -> None:
        self._quest_registry: Dict[str, QuestDefinition] = {}
        self._milestone_registry: Dict[str, MilestoneTier] = {}
        # player_id -> quest_id -> PlayerQuestProgress
        self._player_quests: Dict[str, Dict[str, PlayerQuestProgress]] = {}
        # player_id -> set of claimed milestone_ids
        self._player_milestones: Dict[str, Set[str]] = {}
        self._player_levels: Dict[str, int] = {}
        self._story_engine: Optional[Any] = None

    def attach_story_engine(self, story_engine: Any) -> None:
        self._story_engine = story_engine

    def get_story_engine(self) -> Optional[Any]:
        return self._story_engine

    def register_quest(self, quest_def: QuestDefinition) -> None:
        self._quest_registry[quest_def.quest_id] = quest_def

    def register_milestone(self, milestone: MilestoneTier) -> None:
        self._milestone_registry[milestone.milestone_id] = milestone

    def get_quest_definition(self, quest_id: str) -> Optional[QuestDefinition]:
        return self._quest_registry.get(quest_id)

    def get_milestone(self, milestone_id: str) -> Optional[MilestoneTier]:
        return self._milestone_registry.get(milestone_id)

    def initialize_player(self, player_id: str, player_level: Optional[int] = None) -> None:
        if player_id not in self._player_quests:
            self._player_quests[player_id] = {}
        if player_id not in self._player_milestones:
            self._player_milestones[player_id] = set()
        if player_level is not None:
            self._player_levels[player_id] = player_level
        elif player_id not in self._player_levels:
            self._player_levels[player_id] = 1

    def get_player_quest(self, player_id: str, quest_id: str) -> Optional[PlayerQuestProgress]:
        return self._player_quests.get(player_id, {}).get(quest_id)

    def get_active_quests(self, player_id: str) -> List[PlayerQuestProgress]:
        player_dict = self._player_quests.get(player_id, {})
        return [q for q in player_dict.values() if q.status == QuestStatus.IN_PROGRESS]

    def get_available_quests(self, player_id: str, player_level: int) -> List[QuestDefinition]:
        """Returns quests that the player can accept, excluding hidden and in-progress/completed ones."""
        self.initialize_player(player_id, player_level)
        available: List[QuestDefinition] = []
        player_dict = self._player_quests.get(player_id, {})

        for quest_def in self._quest_registry.values():
            if quest_def.quest_type == QuestType.HIDDEN:
                continue
            if player_level < quest_def.required_level:
                continue

            if quest_def.quest_id in player_dict:
                status = player_dict[quest_def.quest_id].status
                if status in (QuestStatus.IN_PROGRESS, QuestStatus.COMPLETED, QuestStatus.CLAIMED):
                    continue

            prereqs_met = True
            for prereq_id in quest_def.prerequisite_quest_ids:
                prereq_progress = player_dict.get(prereq_id)
                if not prereq_progress or prereq_progress.status not in (QuestStatus.COMPLETED, QuestStatus.CLAIMED):
                    prereqs_met = False
                    break

            if prereqs_met:
                available.append(quest_def)

        return available

    def accept_quest(self, player_id: str, quest_id: str, player_level: int) -> bool:
        """Validates prerequisites and activates a quest for the player."""
        self.initialize_player(player_id, player_level)
        quest_def = self.get_quest_definition(quest_id)
        if not quest_def or quest_def.quest_type == QuestType.HIDDEN:
            return False

        if player_level < quest_def.required_level:
            return False

        player_dict = self._player_quests[player_id]
        if quest_id in player_dict:
            status = player_dict[quest_id].status
            if status in (QuestStatus.IN_PROGRESS, QuestStatus.COMPLETED, QuestStatus.CLAIMED):
                return False

        for prereq_id in quest_def.prerequisite_quest_ids:
            prereq_progress = player_dict.get(prereq_id)
            if not prereq_progress or prereq_progress.status not in (QuestStatus.COMPLETED, QuestStatus.CLAIMED):
                return False

        obj_map = {obj.objective_id: obj for obj in quest_def.objectives}
        progress = PlayerQuestProgress(
            player_id=player_id,
            quest_id=quest_id,
            status=QuestStatus.IN_PROGRESS,
            objectives=obj_map,
            started_at=time.time(),
        )
        player_dict[quest_id] = progress
        return True

    def trigger_hidden_quest(
        self,
        player_id: str,
        trigger_type: HiddenTriggerType,
        context: Optional[Dict[str, Any]] = None,
        **kwargs: Any,
    ) -> Optional[str]:
        """Evaluates cryptic environmental and combat triggers to unlock hidden quests."""
        self.initialize_player(player_id)
        player_dict = self._player_quests[player_id]
        player_level = self._player_levels.get(player_id, 1)

        merged_context = dict(context or {})
        merged_context.update(kwargs)
        context = merged_context

        for quest_def in self._quest_registry.values():
            if quest_def.quest_type != QuestType.HIDDEN:
                continue

            if not quest_def.hidden_trigger or quest_def.hidden_trigger.trigger_type != trigger_type:
                continue

            if player_level < quest_def.required_level:
                continue

            if quest_def.quest_id in player_dict:
                status = player_dict[quest_def.quest_id].status
                if status in (QuestStatus.IN_PROGRESS, QuestStatus.COMPLETED, QuestStatus.CLAIMED):
                    continue

            trigger = quest_def.hidden_trigger
            if trigger_type == HiddenTriggerType.EVASION_NEAR_DEATH:
                zone = context.get("zone")
                hp_percent = context.get("hp_percent", 1.0)
                if trigger.required_zone and zone != trigger.required_zone:
                    continue
                if trigger.required_hp_percent is not None and hp_percent > trigger.required_hp_percent:
                    continue

            elif trigger_type == HiddenTriggerType.RELIC_OFFERING:
                zone = context.get("zone")
                item_id = context.get("item_id") or context.get("required_item_id") or context.get("offered_item")
                if trigger.required_zone and zone != trigger.required_zone:
                    continue
                if trigger.required_item_id and item_id != trigger.required_item_id:
                    continue

            elif trigger_type == HiddenTriggerType.SECRET_ALTAR_INTERACTION:
                zone = context.get("zone")
                if trigger.required_zone and zone != trigger.required_zone:
                    continue

            elif trigger_type == HiddenTriggerType.FIVE_ELEMENTS_STREAK:
                if not context.get("element_streak", False):
                    continue

            elif trigger_type == HiddenTriggerType.NPC_DIALOGUE_CHOICE:
                if trigger.required_zone and context.get("zone") != trigger.required_zone:
                    continue
                if trigger.required_item_id and context.get("item_id") != trigger.required_item_id:
                    continue

            elif trigger_type == HiddenTriggerType.NPC_GIFT_OFFERING:
                item_id = context.get("item_id") or context.get("offered_item")
                if trigger.required_item_id and item_id != trigger.required_item_id:
                    continue
                if trigger.required_zone and context.get("zone") != trigger.required_zone:
                    continue

            obj_map = {obj.objective_id: obj for obj in quest_def.objectives}
            progress = PlayerQuestProgress(
                player_id=player_id,
                quest_id=quest_def.quest_id,
                status=QuestStatus.IN_PROGRESS,
                objectives=obj_map,
                started_at=time.time(),
            )
            player_dict[quest_def.quest_id] = progress
            return quest_def.quest_id

        return None

    def _is_target_match(self, obj_target: str, input_target: str) -> bool:
        if obj_target == input_target:
            return True
        if obj_target == "mob_trash_bone_hound":
            starter_tokens = (
                "bat", "hound", "cẩu", "dơi", "khô lâu", "skeleton", "raptor", "hellhound",
                "mob_starter_bat", "mob_starter_hound", "mob_starter_skeleton",
                "mob_corrupted_raptor", "mob_feral_hellhound", "mob_skeleton_warrior"
            )
            low = input_target.lower()
            return any(tok in low for tok in starter_tokens)
        return False

    def _update_objectives(
        self,
        player_id: str,
        obj_type: ObjectiveType,
        target_id: str,
        amount: int = 1,
    ) -> List[str]:
        """Internal helper to increment progress on matching objectives across active quests."""
        updated_quest_ids: List[str] = []
        player_dict = self._player_quests.get(player_id, {})

        for quest_progress in player_dict.values():
            if quest_progress.status != QuestStatus.IN_PROGRESS:
                continue

            quest_updated = False
            for obj_id, obj in quest_progress.objectives.items():
                if obj.obj_type == obj_type and self._is_target_match(obj.target_id, target_id):
                    if not obj.is_completed:
                        if obj_type == ObjectiveType.CLEAR_FOG_PERCENT:
                            quest_progress.objectives[obj_id] = obj.with_gauge_value(amount)
                        else:
                            quest_progress.objectives[obj_id] = obj.with_increment(amount)
                        quest_updated = True

            if quest_updated:
                all_done = all(o.is_completed for o in quest_progress.objectives.values())
                if all_done:
                    quest_progress.status = QuestStatus.COMPLETED
                    quest_progress.completed_at = time.time()
                    if self._story_engine:
                        self._story_engine.on_quest_completed(player_id, quest_progress.quest_id)
                updated_quest_ids.append(quest_progress.quest_id)

        return updated_quest_ids

    def record_kill(self, player_id: str, monster_id: str, count: int = 1) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.KILL_MONSTER, monster_id, count)

    def record_craft(self, player_id: str, item_id: str, count: int = 1) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.CRAFT_ITEM, item_id, count)

    def record_discovery(self, player_id: str, location_id: str) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.DISCOVER_LOCATION, location_id, 1)

    def record_meridian_unlock(self, player_id: str, acupoint_id: str) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.ACTIVATE_MERIDIAN, acupoint_id, 1)

    def record_action(self, player_id: str, action_key: str, count: int = 1) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.PERFORM_ACTION, action_key, count)

    def record_talk_npc(self, player_id: str, npc_id: str) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.TALK_NPC, npc_id, 1)

    def record_deliver_npc(self, player_id: str, npc_id: str, count: int = 1) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.DELIVER_TO_NPC, npc_id, count)

    def record_deliver_to_npc(self, player_id: str, npc_id: str, count: int = 1) -> List[str]:
        return self.record_deliver_npc(player_id, npc_id, count)

    def record_room_exploration(self, player_id: str, target_room_id: str, count: int = 1) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.EXPLORE_MAP_ROOMS, target_room_id, count)

    def record_fog_clearance(self, player_id: str, zone_or_map_id: str, percent: int) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.CLEAR_FOG_PERCENT, zone_or_map_id, percent)

    def record_boss_gate_breach(self, player_id: str, gate_target_id: str) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.BREACH_BOSS_GATE, gate_target_id, 1)

    def record_obstacle_destruction(self, player_id: str, obstacle_target_id: str, count: int = 1) -> List[str]:
        return self._update_objectives(player_id, ObjectiveType.DESTROY_OBSTACLES, obstacle_target_id, count)

    def claim_quest_reward(self, player_id: str, quest_id: str) -> Tuple[bool, Optional[QuestReward], str]:
        """Claims reward for completed quest. Guarantees idempotency and anti-double claim."""
        self.initialize_player(player_id)
        player_dict = self._player_quests[player_id]
        quest_progress = player_dict.get(quest_id)

        if not quest_progress:
            return False, None, "Nhiệm vụ không tồn tại trong tiến trình người chơi!"
        if quest_progress.status == QuestStatus.CLAIMED:
            return False, None, "Phần thưởng nhiệm vụ này đã nhận trước đó!"
        if quest_progress.status != QuestStatus.COMPLETED:
            return False, None, "Nhiệm vụ chưa hoàn thành mục tiêu!"

        quest_def = self.get_quest_definition(quest_id)
        if not quest_def:
            return False, None, "Không tìm thấy định nghĩa nhiệm vụ!"

        quest_progress.status = QuestStatus.CLAIMED
        quest_progress.claimed_at = time.time()
        if self._story_engine:
            self._story_engine.on_quest_completed(player_id, quest_id)
        return True, quest_def.reward, "Nhận thưởng nhiệm vụ thành công!"

    def get_player_milestone_status(self, player_id: str) -> Dict[str, Any]:
        """Calculates completed quests statistics and identifies eligible milestones."""
        self.initialize_player(player_id)
        player_dict = self._player_quests.get(player_id, {})
        claimed_milestones = self._player_milestones.get(player_id, set())

        total_completed = 0
        hidden_completed = 0
        high_tier_completed = 0

        for q_id, q_prog in player_dict.items():
            if q_prog.status in (QuestStatus.COMPLETED, QuestStatus.CLAIMED):
                quest_def = self.get_quest_definition(q_id)
                if not quest_def:
                    continue
                total_completed += 1
                if quest_def.quest_type == QuestType.HIDDEN:
                    hidden_completed += 1
                elif quest_def.quest_type == QuestType.HIGH_TIER:
                    high_tier_completed += 1

        eligible_milestones: List[str] = []
        for ms_id, ms in self._milestone_registry.items():
            if ms_id in claimed_milestones:
                continue
            if (
                total_completed >= ms.required_total_completed
                and hidden_completed >= ms.required_hidden_completed
                and high_tier_completed >= ms.required_high_tier_completed
            ):
                eligible_milestones.append(ms_id)

        return {
            "player_id": player_id,
            "total_completed": total_completed,
            "hidden_completed": hidden_completed,
            "high_tier_completed": high_tier_completed,
            "claimed_milestones": list(claimed_milestones),
            "eligible_milestones": eligible_milestones,
        }

    def claim_milestone_reward(
        self, player_id: str, milestone_id: str
    ) -> Tuple[bool, Optional[QuestReward], str]:
        """Claims reward for a qualified milestone tier. Guarantees anti-double claiming."""
        self.initialize_player(player_id)
        milestone = self.get_milestone(milestone_id)
        if not milestone:
            return False, None, "Không tìm thấy thông tin mốc phần thưởng!"

        claimed_set = self._player_milestones[player_id]
        if milestone_id in claimed_set:
            return False, None, "Mốc phần thưởng này đã nhận trước đó!"

        status = self.get_player_milestone_status(player_id)
        if milestone_id not in status["eligible_milestones"]:
            return False, None, "Chưa đạt điều kiện mốc hoàn thành nhiệm vụ!"

        claimed_set.add(milestone_id)
        if self._story_engine:
            self._story_engine.on_quest_completed(player_id, milestone_id)
        return True, milestone.reward, "Nhận phần thưởng mốc thành công!"


def create_default_quest_engine(attach_storyline: bool = True) -> QuestEngine:
    """Factory creating a QuestEngine populated with canonical FreeExile quests and milestones."""
    engine = QuestEngine()
    register_canonical_quests_and_milestones(engine)
    if attach_storyline:
        try:
            from server.world.storyline_engine import create_default_storyline_engine
            story_engine = create_default_storyline_engine()
            engine.attach_story_engine(story_engine)
        except Exception:
            pass
    return engine
