"""Ouroboros: Cyclical reset boundaries for open deviation bars.

Named after the Greek serpent eating its tail (οὐροβόρος), representing the
cyclical nature of daily reset boundaries.

This module provides:
- Boundary calculation for day granularity (sole production mode)
- Orphaned bar metadata for ML filtering
- Exchange session detection (Sydney/Tokyo/London/New York)
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC, date, datetime, timedelta
from enum import Enum
from typing import TYPE_CHECKING, Literal

if TYPE_CHECKING:
    from collections.abc import Iterator

# ============================================================================
# Types
# ============================================================================


class OuroborosMode(str, Enum):
    """Ouroboros granularity mode.

    DAY is the only supported mode. The processor resets at UTC midnight,
    making each day independently reprocessable and cache-friendly.
    """

    DAY = "day"


@dataclass(frozen=True)
class OuroborosBoundary:
    """A single ouroboros reset boundary."""

    timestamp: datetime
    """UTC datetime of the boundary."""

    mode: OuroborosMode
    """Which granularity created this boundary."""

    reason: str
    """Human-readable reason (e.g., 'day_boundary')."""

    @property
    def timestamp_ms(self) -> int:
        """Timestamp in milliseconds (for comparison with trade data)."""
        return int(self.timestamp.timestamp() * 1000)

    @property
    def timestamp_us(self) -> int:
        """Timestamp in microseconds."""
        return int(self.timestamp.timestamp() * 1_000_000)


@dataclass
class OrphanedBarMetadata:
    """Metadata for orphaned bars at ouroboros boundaries."""

    is_orphan: bool = True
    """Always True for orphaned bars."""

    ouroboros_boundary: datetime | None = None
    """Which boundary caused the orphan."""

    reason: str | None = None
    """Reason string: 'day_boundary'."""

    expected_duration_us: int | None = None
    """Expected duration if bar had completed normally."""


# ============================================================================
# Boundary Calculation
# ============================================================================


def get_ouroboros_boundaries(
    start: date,
    end: date,
    mode: Literal["day"] = "day",
) -> list[OuroborosBoundary]:
    """Return all ouroboros reset points within the date range.

    Parameters
    ----------
    start : date
        Start date (inclusive)
    end : date
        End date (inclusive)
    mode : {"day"}
        Ouroboros granularity (only day mode supported)

    Returns
    -------
    list[OuroborosBoundary]
        Sorted list of boundaries within the date range

    Examples
    --------
    >>> from datetime import date
    >>> boundaries = get_ouroboros_boundaries(
    ...     date(2024, 1, 1), date(2024, 1, 3)
    ... )
    >>> len(boundaries)
    3
    >>> boundaries[0].reason
    'day_boundary'
    """
    if mode != "day":
        msg = (
            f"Invalid ouroboros mode: {mode!r}. Only 'day' mode is supported. "
            "Year/month/week modes were removed in v12.66."
        )
        raise ValueError(msg)

    boundaries: list[OuroborosBoundary] = []
    current = start
    while current <= end:
        boundaries.append(
            OuroborosBoundary(
                timestamp=datetime(
                    current.year,
                    current.month,
                    current.day,
                    0,
                    0,
                    0,
                    tzinfo=UTC,
                ),
                mode=OuroborosMode.DAY,
                reason="day_boundary",
            )
        )
        current += timedelta(days=1)

    return boundaries


def iter_ouroboros_segments(
    start: date,
    end: date,
    mode: Literal["day"] = "day",
) -> Iterator[tuple[datetime, datetime, OuroborosBoundary | None]]:
    """Iterate over date segments between ouroboros boundaries.

    Yields (segment_start, segment_end, boundary) tuples where boundary
    is the ouroboros boundary at segment_start (None for first segment
    if it doesn't start on a boundary).

    Parameters
    ----------
    start : date
        Start date
    end : date
        End date
    mode : {"day"}
        Ouroboros granularity (only day mode supported)

    Yields
    ------
    tuple[datetime, datetime, OuroborosBoundary | None]
        (segment_start, segment_end, boundary_at_start)
    """
    boundaries = get_ouroboros_boundaries(start, end, mode)

    # Convert dates to datetimes
    start_dt = datetime(start.year, start.month, start.day, 0, 0, 0, tzinfo=UTC)
    end_dt = datetime(end.year, end.month, end.day, 23, 59, 59, 999999, tzinfo=UTC)

    if not boundaries:
        # No boundaries in range - single segment
        yield (start_dt, end_dt, None)
        return

    # First segment: start to first boundary (if start is before first boundary)
    if start_dt < boundaries[0].timestamp:
        yield (start_dt, boundaries[0].timestamp - timedelta(microseconds=1), None)

    # Middle segments: between consecutive boundaries
    for i, boundary in enumerate(boundaries):
        if i + 1 < len(boundaries):
            segment_end = boundaries[i + 1].timestamp - timedelta(microseconds=1)
        else:
            segment_end = end_dt

        # Only yield if segment start is before segment end
        if boundary.timestamp <= end_dt:
            yield (boundary.timestamp, segment_end, boundary)


# ============================================================================
# Exchange Market Sessions
# ============================================================================

# Market session hours in local time (aligned with actual exchange hours)
# Note: These are approximate for crypto; traditional markets have pre/post sessions
# Issue #8: Exchange sessions integration (corrected per exchange schedules)
EXCHANGE_SESSION_HOURS = {
    "sydney": {"tz": "Australia/Sydney", "start": 10, "end": 16},  # ASX
    "tokyo": {"tz": "Asia/Tokyo", "start": 9, "end": 15},  # TSE
    "london": {"tz": "Europe/London", "start": 8, "end": 17},  # LSE
    "newyork": {"tz": "America/New_York", "start": 10, "end": 16},  # NYSE
}


@dataclass(frozen=True)
class ExchangeSessionFlags:
    """Boolean flags for active exchange market sessions."""

    sydney: bool
    tokyo: bool
    london: bool
    newyork: bool

    def to_dict(self) -> dict[str, bool]:
        """Convert to dict with column names."""
        return {
            "exchange_session_sydney": self.sydney,
            "exchange_session_tokyo": self.tokyo,
            "exchange_session_london": self.london,
            "exchange_session_newyork": self.newyork,
        }


def get_active_exchange_sessions(timestamp_utc: datetime) -> ExchangeSessionFlags:
    """Determine which exchange market sessions are active at a given UTC time.

    Parameters
    ----------
    timestamp_utc : datetime
        UTC datetime to check (must be timezone-aware)

    Returns
    -------
    ExchangeSessionFlags
        Boolean flags for each session

    Notes
    -----
    This is a simplified implementation that uses fixed hours.
    For production use with DST accuracy, consider using nautilus_trader's
    ForexSession implementation.
    """
    import zoneinfo

    def is_in_session(session_name: str) -> bool:
        info = EXCHANGE_SESSION_HOURS[session_name]
        tz = zoneinfo.ZoneInfo(info["tz"])
        local_time = timestamp_utc.astimezone(tz)
        hour = local_time.hour
        # Skip weekends
        if local_time.weekday() >= 5:
            return False
        return info["start"] <= hour < info["end"]

    return ExchangeSessionFlags(
        sydney=is_in_session("sydney"),
        tokyo=is_in_session("tokyo"),
        london=is_in_session("london"),
        newyork=is_in_session("newyork"),
    )


# ============================================================================
# Validation
# ============================================================================


def validate_ouroboros_mode(mode: str) -> Literal["day"]:
    """Validate ouroboros mode string.

    Parameters
    ----------
    mode : str
        Mode to validate

    Returns
    -------
    Literal["day"]
        Validated mode (always "day")

    Raises
    ------
    ValueError
        If mode is not "day"
    """
    if mode != "day":
        msg = (
            f"Invalid ouroboros mode: {mode!r}. Only 'day' mode is supported. "
            "Year/month/week modes were removed in v12.66."
        )
        raise ValueError(msg)
    return mode  # type: ignore[return-value]


def get_operational_ouroboros_mode() -> Literal["day"]:
    """Resolve the operational ouroboros mode from Settings.  # Issue #126

    Reads ``OPENDEVIATIONBAR_OUROBOROS_MODE`` via the unified Settings singleton
    and validates it. This is the single source of truth for resolving
    the default ouroboros mode at runtime.

    Returns
    -------
    Literal["day"]
        Always returns "day" (sole supported mode).

    Raises
    ------
    ValueError
        If the configured mode is not "day".
    """
    from opendeviationbar.config import Settings

    return validate_ouroboros_mode(Settings.get().population.ouroboros_mode)
