#!/usr/bin/env python3
# File: src/noveler/infrastructure/repositories/yaml_character_repository.py
# Purpose: Manage character profiles stored in YAML files (A24 schema + legacy array schema).
# Context: Infrastructure repository used by application services and CLI tooling.

"""YAML-backed character repository with A24 schema compatibility."""

from __future__ import annotations

from collections.abc import Iterable
from pathlib import Path
from typing import Any

import yaml

from noveler.domain.adapters.character_profile_adapter import (
    CharacterBookEntry,
    CharacterProfileAdapter,
)
from noveler.domain.repositories.character_repository import CharacterRepository
from noveler.domain.value_objects.character_profile import AttributeValue, CharacterProfile

CHARACTER_FILE_NAME = "キャラクター.yaml"
CHARACTER_BOOK_ROOT = "character_book"
CHARACTER_ARRAY_KEY = "characters"


class YamlCharacterRepository(CharacterRepository):
    """Repository that loads and persists character profiles in YAML."""

    def __init__(self, base_path: Path | str) -> None:
        self.base_path = Path(base_path)
        self.character_file = self.base_path / "30_設定集" / CHARACTER_FILE_NAME

    # ---------------------------------------------------------------- lookups --
    def find_all_by_project(self, _project_name: str) -> list[CharacterProfile]:
        data = self._read_yaml()
        if not data:
            return []

        profiles: list[CharacterProfile] = []

        book_section = data.get(CHARACTER_BOOK_ROOT)
        if isinstance(book_section, dict):
            profiles.extend(self._load_from_character_book(book_section))

        legacy_array = data.get(CHARACTER_ARRAY_KEY)
        if isinstance(legacy_array, list):
            profiles.extend(self._load_from_legacy_array(legacy_array))

        return profiles

    def find_by_name(self, project_name: str, name: str) -> CharacterProfile | None:
        for profile in self.find_all_by_project(project_name):
            if profile.name == name:
                return profile
        return None

    def find_by_attribute(self, project_name: str, attribute: str, value: object) -> list[CharacterProfile]:
        return [
            profile
            for profile in self.find_all_by_project(project_name)
            if profile.get_attribute(attribute) == value
        ]

    def exists(self, project_name: str, name: str) -> bool:
        return self.find_by_name(project_name, name) is not None

    # ---------------------------------------------------------------- persistence --
    def save(self, _project_name: str, character: CharacterProfile) -> None:
        data = self._read_yaml() or {}

        if CHARACTER_BOOK_ROOT in data and isinstance(data[CHARACTER_BOOK_ROOT], dict):
            self._upsert_character_book_entry(data[CHARACTER_BOOK_ROOT], character)
        else:
            self._upsert_legacy_entry(data, character)

        self._write_yaml(data)

    def delete(self, _project_name: str, name: str) -> bool:
        data = self._read_yaml()
        if not data:
            return False

        removed = False

        if CHARACTER_BOOK_ROOT in data and isinstance(data[CHARACTER_BOOK_ROOT], dict):
            removed = self._delete_from_character_book(data[CHARACTER_BOOK_ROOT], name)

        if not removed and CHARACTER_ARRAY_KEY in data:
            removed = self._delete_from_legacy_array(data, name)

        if removed:
            self._write_yaml(data)
        return removed

    # ------------------------------------------------------------ character book --
    def _load_from_character_book(self, book: dict[str, Any]) -> list[CharacterProfile]:
        characters = book.get("characters")
        if not isinstance(characters, dict):
            return []

        profiles: list[CharacterProfile] = []
        for category, entries in characters.items():
            if not isinstance(entries, list):
                continue
            for entry in entries:
                if not isinstance(entry, dict):
                    continue

                status = dict(entry.get("status") or {})
                status.setdefault("lifecycle", "active")

                character_id = str(
                    entry.get("character_id")
                    or entry.get("id")
                    or entry.get("name")
                    or f"{category}_{len(profiles)}"
                )
                display_name = str(entry.get("display_name") or entry.get("name") or character_id)

                raw_layers = entry.get("layers")
                if isinstance(raw_layers, dict):
                    layers = {key: value for key, value in raw_layers.items() if isinstance(value, dict)}
                else:
                    layers = {}

                if layers:
                    adapter_entry = CharacterBookEntry(
                        character_id=character_id,
                        display_name=display_name,
                        status=status,
                        layers=layers,
                        narrative_notes=entry.get("narrative_notes")
                        if isinstance(entry.get("narrative_notes"), dict)
                        else None,
                        llm_prompt_profile=entry.get("llm_prompt_profile")
                        if isinstance(entry.get("llm_prompt_profile"), dict)
                        else None,
                        logging=entry.get("logging") if isinstance(entry.get("logging"), dict) else None,
                        lite_profile_hint=entry.get("lite_profile_hint")
                        if isinstance(entry.get("lite_profile_hint"), dict)
                        else None,
                        episode_snapshots=entry.get("episode_snapshots")
                        if isinstance(entry.get("episode_snapshots"), list)
                        else [],
                    )

                    profile = CharacterProfileAdapter.from_character_book_entry(adapter_entry)

                    legacy_overrides = self._extract_flat_overrides(entry)
                    legacy_overrides.setdefault("category_group", category)
                    if legacy_overrides:
                        merged_attributes = profile.to_dict()["attributes"]
                        merged_attributes.update(legacy_overrides)
                        profile = CharacterProfile(profile.name, merged_attributes)
                else:
                    attributes = self._extract_flat_overrides(entry)
                    attributes.setdefault("character_id", character_id)
                    attributes.setdefault("category", status.get("lifecycle", "active"))
                    attributes.setdefault("category_group", category)
                    profile = CharacterProfile(display_name, attributes)

                profiles.append(profile)

        return profiles

    def _upsert_character_book_entry(self, book: dict[str, Any], profile: CharacterProfile) -> None:
        characters = book.setdefault("characters", {})
        if not isinstance(characters, dict):
            characters = {}
            book["characters"] = characters

        category_group = str(
            profile.get_attribute("category_group")
            or profile.get_attribute("category")
            or "supporting"
        )
        lifecycle = str(profile.get_attribute("category") or "active")
        character_id = str(profile.get_attribute("character_id") or profile.name)
        display_name = profile.name
        layers = profile.get_attribute("layers")
        if not isinstance(layers, dict):
            layers = {}

        entry = {
            "character_id": character_id,
            "display_name": display_name,
            "status": {"lifecycle": lifecycle},
            "layers": layers,
        }

        category_entries = characters.setdefault(category_group, [])
        if not isinstance(category_entries, list):
            category_entries = []
            characters[category_group] = category_entries

        for index, existing in enumerate(category_entries):
            if isinstance(existing, dict) and existing.get("character_id") == character_id:
                category_entries[index] = entry
                break
        else:
            category_entries.append(entry)

    def _delete_from_character_book(self, book: dict[str, Any], name: str) -> bool:
        characters = book.get("characters")
        if not isinstance(characters, dict):
            return False

        removed = False
        for category, entries in characters.items():
            if not isinstance(entries, list):
                continue
            filtered = [
                entry
                for entry in entries
                if not (
                    isinstance(entry, dict)
                    and (entry.get("display_name") == name or entry.get("name") == name)
                )
            ]
            if len(filtered) != len(entries):
                characters[category] = filtered
                removed = True
        return removed

    # -------------------------------------------------------------- legacy schema --
    def _load_from_legacy_array(self, entries: Iterable[Any]) -> list[CharacterProfile]:
        profiles: list[CharacterProfile] = []
        for entry in entries:
            if not isinstance(entry, dict):
                continue
            name = str(entry.get("name") or entry.get("display_name") or f"character_{len(profiles)}")
            attributes = entry.get("attributes") if isinstance(entry.get("attributes"), dict) else {}
            profile = CharacterProfile(name=name, attributes=attributes)
            profiles.append(profile)
        return profiles

    def _upsert_legacy_entry(self, data: dict[str, Any], profile: CharacterProfile) -> None:
        characters = data.setdefault(CHARACTER_ARRAY_KEY, [])
        if not isinstance(characters, list):
            characters = []
            data[CHARACTER_ARRAY_KEY] = characters

        record = {"name": profile.name, "attributes": profile.attributes}
        for idx, existing in enumerate(characters):
            if isinstance(existing, dict) and existing.get("name") == profile.name:
                characters[idx] = record
                break
        else:
            characters.append(record)

    def _delete_from_legacy_array(self, data: dict[str, Any], name: str) -> bool:
        characters = data.get(CHARACTER_ARRAY_KEY)
        if not isinstance(characters, list):
            return False
        filtered = [entry for entry in characters if not (isinstance(entry, dict) and entry.get("name") == name)]
        if len(filtered) == len(characters):
            return False
        data[CHARACTER_ARRAY_KEY] = filtered
        return True

    # -------------------------------------------------------------- utilities --
    def _extract_flat_overrides(self, entry: dict[str, Any]) -> dict[str, AttributeValue]:
        overrides: dict[str, AttributeValue] = {}
        for key in ("personality", "hair_color", "eye_color", "height", "speech_style", "catchphrase"):
            value = entry.get(key)
            if isinstance(value, str | list) and value:
                overrides[key] = value

        likes = self._coerce_list(entry.get("likes"))
        dislikes = self._coerce_list(entry.get("dislikes"))
        goals = self._coerce_list(entry.get("goals"))
        if likes:
            overrides["likes"] = likes
        if dislikes:
            overrides["dislikes"] = dislikes
        if goals:
            overrides["goals"] = goals

        traits = self._coerce_list(entry.get("traits"))
        traits_pos = self._coerce_list(entry.get("traits_positive"))
        traits_neg = self._coerce_list(entry.get("traits_negative"))
        combined_traits = traits or (traits_pos + traits_neg)
        if combined_traits:
            overrides["traits"] = combined_traits
        return overrides

    @staticmethod
    def _coerce_list(value: Any) -> list[str]:
        if isinstance(value, list):
            return [str(item) for item in value]
        if isinstance(value, str) and value:
            return [value]
        return []

    def _read_yaml(self) -> dict[str, Any] | None:
        if not self.character_file.exists():
            return None
        try:
            with self.character_file.open(encoding="utf-8") as handle:
                data = yaml.safe_load(handle)
            return data if isinstance(data, dict) else {}
        except Exception:
            return {}

    def _write_yaml(self, data: dict[str, Any]) -> None:
        self.character_file.parent.mkdir(parents=True, exist_ok=True)
        with self.character_file.open("w", encoding="utf-8") as handle:
            yaml.safe_dump(data, handle, allow_unicode=True, sort_keys=False)


__all__ = ["YamlCharacterRepository"]
