"""
Shared attribute classes used across multiple namespaces.

This module contains data structures that are shared between different
namespaces (tarot, letter, number, temporal, kaballah) and don't belong
exclusively to any single namespace.
"""

from dataclasses import dataclass, field
from typing import Dict, List, Optional, Sequence, Set, Tuple


@dataclass
class Meaning:
    """Represents the meaning of a card."""

    upright: str
    reversed: str


@dataclass(frozen=True)
class Note:
    """Represents a musical note with its properties."""

    name: str  # e.g., "C", "D", "E", "F#", "G", "A", "B"
    frequency: float  # Frequency in Hz (A4 = 440 Hz)
    semitone: int  # Position in chromatic scale (0-11)
    octave: int = 4  # Default octave
    element: Optional[str] = None  # Associated element if any
    planet: Optional[str] = None  # Associated planet if any
    chakra: Optional[str] = None  # Associated chakra if any
    keywords: List[str] = field(default_factory=list)
    description: str = ""

    def __post_init__(self) -> None:
        if not 0 <= self.semitone <= 11:
            raise ValueError(f"Semitone must be 0-11, got {self.semitone}")
        if not 0 <= self.frequency <= 20000:
            raise ValueError(f"Frequency must be 0-20000 Hz, got {self.frequency}")


@dataclass
class Element:
    """Represents one of the four elements."""

    name: str
    symbol: str
    color: str
    direction: str
    astrological_signs: List[str] = field(default_factory=list)


@dataclass
class ElementType:
    """Represents an elemental force (Fire, Water, Air, Earth, Spirit)."""

    name: str
    symbol: str
    direction: str
    polarity: str  # Active, Passive, or Neutral
    color: str
    tarot_suits: List[str] = field(default_factory=list)
    zodiac_signs: List[str] = field(default_factory=list)
    keywords: List[str] = field(default_factory=list)
    description: str = ""

    def __post_init__(self) -> None:
        if self.polarity not in {"Active", "Passive", "Neutral"}:
            raise ValueError(f"Polarity must be Active, Passive, or Neutral, got {self.polarity}")


@dataclass
class Number:
    """Represents a number (1-9) with Kabbalistic attributes."""

    value: int
    sephera: str
    element: str
    compliment: int
    color: Optional["Color"] = None

    def __post_init__(self) -> None:
        if not (1 <= self.value <= 9):
            raise ValueError(f"Number value must be between 1 and 9, got {self.value}")

        # Auto-calculate compliment: numbers complement to sum to 9
        # 1↔8, 2↔7, 3↔6, 4↔5, 9↔9
        self.compliment = 9 - self.value if self.value != 9 else 9


@dataclass(frozen=True)
class Colorscale:
    """
    Represents Golden Dawn color scales (King, Queen, Emperor, Empress).

    The four scales correspond to the four worlds/letters of Tetragrammaton:
    - King Scale (Yod): Father, originating impulse, pure archetype
    - Queen Scale (He): Mother, receptive, earthy counterpart
    - Emperor Scale (Vau): Son/Form, active expression, concrete manifestation
    - Empress Scale (He final): Daughter, physical manifestation, receptivity in Assiah
    """

    name: str  # Sephira/Path name (e.g., "Kether", "Path of Aleph")
    number: int  # 1-10 for Sephiroth, 11-32 for Paths
    king_scale: str  # Yod - Father principle
    queen_scale: str  # He - Mother principle
    emperor_scale: str  # Vau - Son principle
    empress_scale: str  # He final - Daughter principle
    sephirotic_color: Optional[str] = None  # Collective color of Sephiroth
    type: str = "Sephira"  # "Sephira" or "Path"
    keywords: List[str] = field(default_factory=list)
    description: str = ""


@dataclass
class Color:
    """Represents a color with Kabbalistic correspondences."""

    name: str
    hex_value: str
    rgb: Tuple[int, int, int]
    sephera: str
    number: int
    element: str
    scale: str  # King, Queen, Prince, Princess scale
    meaning: str
    tarot_associations: List[str] = field(default_factory=list)
    description: str = ""

    def __post_init__(self) -> None:
        # Validate hex color
        if not self.hex_value.startswith("#") or len(self.hex_value) != 7:
            raise ValueError(f"Invalid hex color: {self.hex_value}")

        # Validate RGB values
        if not all(0 <= c <= 255 for c in self.rgb):
            raise ValueError(f"RGB values must be between 0 and 255, got {self.rgb}")


@dataclass
class Planet:
    """Represents a planetary correspondence entry."""

    name: str
    symbol: str
    element: str
    ruling_zodiac: List[str] = field(default_factory=list)
    associated_numbers: List[Number] = field(default_factory=list)
    associated_letters: List[str] = field(default_factory=list)
    keywords: List[str] = field(default_factory=list)
    color: Optional[Color] = None
    description: str = ""

    def __str__(self) -> str:
        """Return nicely formatted string representation of the Planet."""
        lines = []
        lines.append(f"{self.name} ({self.symbol})")
        lines.append(f"  element: {self.element}")

        if self.ruling_zodiac:
            lines.append(f"  ruling_zodiac: {', '.join(self.ruling_zodiac)}")

        if self.associated_letters:
            lines.append(f"  associated_letters: {', '.join(self.associated_letters)}")

        if self.keywords:
            lines.append(f"  keywords: {', '.join(self.keywords)}")

        if self.color:
            lines.append(f"  color: {self.color.name}")

        if self.description:
            lines.append(f"  description: {self.description}")

        return "\n".join(lines)


@dataclass
class God:
    """Unified deity representation that synchronizes multiple pantheons."""

    name: str
    culture: str
    pantheon: str
    domains: List[str] = field(default_factory=list)
    epithets: List[str] = field(default_factory=list)
    mythology: str = ""
    sephera_numbers: List[int] = field(default_factory=list)
    path_numbers: List[int] = field(default_factory=list)
    planets: List[str] = field(default_factory=list)
    elements: List[str] = field(default_factory=list)
    zodiac_signs: List[str] = field(default_factory=list)
    associated_planet: Optional[Planet] = None
    associated_element: Optional[ElementType] = None
    associated_numbers: List[Number] = field(default_factory=list)
    tarot_trumps: List[str] = field(default_factory=list)
    keywords: List[str] = field(default_factory=list)
    description: str = ""

    def culture_key(self) -> str:
        """Return a normalized key for dictionary grouping."""
        return self.culture.lower()

    def primary_number(self) -> Optional[Number]:
        """Return the first associated number if one is available."""
        return self.associated_numbers[0] if self.associated_numbers else None

    def __str__(self) -> str:
        """Return nicely formatted string representation of the God."""
        lines = []
        lines.append(f"{self.name}")
        lines.append(f"  culture: {self.culture}")
        lines.append(f"  pantheon: {self.pantheon}")

        if self.domains:
            lines.append(f"  domains: {', '.join(self.domains)}")

        if self.epithets:
            lines.append(f"  epithets: {', '.join(self.epithets)}")

        if self.mythology:
            lines.append(f"  mythology: {self.mythology}")

        if self.sephera_numbers:
            lines.append(f"  sephera_numbers: {', '.join(str(n) for n in self.sephera_numbers)}")

        if self.path_numbers:
            lines.append(f"  path_numbers: {', '.join(str(n) for n in self.path_numbers)}")

        if self.planets:
            lines.append(f"  planets: {', '.join(self.planets)}")

        if self.elements:
            lines.append(f"  elements: {', '.join(self.elements)}")

        if self.zodiac_signs:
            lines.append(f"  zodiac_signs: {', '.join(self.zodiac_signs)}")

        if self.associated_planet:
            lines.append(f"  associated_planet: {self.associated_planet.name}")

        if self.associated_element:
            elem_name = (
                self.associated_element.name
                if hasattr(self.associated_element, "name")
                else str(self.associated_element)
            )
            lines.append(f"  associated_element: {elem_name}")

        if self.tarot_trumps:
            lines.append(f"  tarot_trumps: {', '.join(self.tarot_trumps)}")

        if self.keywords:
            lines.append(f"  keywords: {', '.join(self.keywords)}")

        if self.description:
            lines.append(f"  description: {self.description}")

        return "\n".join(lines)


@dataclass
class Perfume:
    """Represents a perfume/incense correspondence in Kabbalah."""

    name: str
    alternative_names: List[str] = field(default_factory=list)
    scent_profile: str = ""  # e.g., "Resinous", "Floral", "Spicy", "Earthy"
    sephera_number: Optional[int] = None  # Sephira number (1-10) if sephera correspondence
    path_number: Optional[int] = None  # Path number (11-32) if path correspondence
    element: Optional[str] = None  # "Air", "Fire", "Water", "Earth", "Spirit"
    planet: Optional[str] = None  # Planetary association
    zodiac_sign: Optional[str] = None  # Zodiac sign association
    astrological_quality: Optional[str] = None  # "Ascendant", "Succedent", "Cadent"
    keywords: List[str] = field(default_factory=list)
    associated_number: Optional[Number] = None
    magical_uses: List[str] = field(default_factory=list)
    description: str = ""
    notes: str = ""

    def __str__(self) -> str:
        """Return nicely formatted string representation of the Perfume."""
        lines = []
        lines.append(f"{self.name}")

        if self.alternative_names:
            lines.append(f"  alternative_names: {', '.join(self.alternative_names)}")

        if self.scent_profile:
            lines.append(f"  scent_profile: {self.scent_profile}")

        # Correspondences
        if self.sephera_number is not None:
            lines.append(f"  sephera_number: {self.sephera_number}")

        if self.path_number is not None:
            lines.append(f"  path_number: {self.path_number}")

        if self.element:
            lines.append(f"  element: {self.element}")

        if self.planet:
            lines.append(f"  planet: {self.planet}")

        if self.zodiac_sign:
            lines.append(f"  zodiac_sign: {self.zodiac_sign}")

        if self.astrological_quality:
            lines.append(f"  astrological_quality: {self.astrological_quality}")

        if self.keywords:
            lines.append(f"  keywords: {', '.join(self.keywords)}")

        if self.magical_uses:
            lines.append(f"  magical_uses: {', '.join(self.magical_uses)}")

        if self.description:
            lines.append(f"  description: {self.description}")

        if self.notes:
            lines.append(f"  notes: {self.notes}")

        return "\n".join(lines)


@dataclass(frozen=True)
class Cipher:
    """Pattern-based cipher that can be layered onto any alphabet order."""

    name: str
    key: str
    pattern: Sequence[int]
    cycle: bool = False
    default_alphabet: Optional[str] = None
    letter_subset: Optional[Set[str]] = None
    description: str = ""

    def __post_init__(self) -> None:
        if not self.pattern:
            raise ValueError("Cipher pattern must include at least one value")
        normalized_subset = None
        if self.letter_subset:
            normalized_subset = {letter.upper() for letter in self.letter_subset}
        object.__setattr__(self, "pattern", tuple(self.pattern))
        object.__setattr__(self, "letter_subset", normalized_subset)

    def mapping_for_alphabet(self, alphabet_letters: Sequence[str]) -> Dict[str, int]:
        """Build a lookup mapping for the provided alphabet order."""
        letters_in_scope: List[str] = []
        for letter in alphabet_letters:
            normalized = letter.upper()
            if self.letter_subset and normalized not in self.letter_subset:
                continue
            letters_in_scope.append(normalized)
        if not letters_in_scope:
            raise ValueError("Alphabet does not contain any letters applicable to this cipher")
        values = self._expand_pattern(len(letters_in_scope))
        return {letter: value for letter, value in zip(letters_in_scope, values)}

    def encode(self, text: str, alphabet_letters: Sequence[str]) -> List[int]:
        """Encode the supplied text using the alphabet ordering."""
        mapping = self.mapping_for_alphabet(alphabet_letters)
        values: List[int] = []
        for char in text.upper():
            if char in mapping:
                values.append(mapping[char])
        return values

    def _expand_pattern(self, target_length: int) -> List[int]:
        if len(self.pattern) == target_length:
            return list(self.pattern)
        if self.cycle:
            expanded: List[int] = []
            idx = 0
            while len(expanded) < target_length:
                expanded.append(self.pattern[idx % len(self.pattern)])
                idx += 1
            return expanded
        raise ValueError("Cipher pattern length does not match alphabet and cycling is disabled")


@dataclass(frozen=True)
class CipherResult:
    """Result of applying a cipher to a word/alphabet combination."""

    word: str
    cipher: Cipher
    alphabet_name: str
    values: Tuple[int, ...]

    @property
    def total(self) -> int:
        return sum(self.values)

    def as_string(self, separator: str = "") -> str:
        return separator.join(str(value) for value in self.values)

    def __str__(self) -> str:
        return self.as_string()

    def __int__(self) -> int:
        return self.total
