"""Checkpoint data models: PopulationContext and PopulationCheckpoint dataclasses."""

from __future__ import annotations

import json
import logging
import os
import tempfile
from dataclasses import asdict, dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

from platformdirs import user_cache_dir

if TYPE_CHECKING:
    from concurrent.futures import ThreadPoolExecutor

    from opendeviationbar.progress import ResumableObserver
    from opendeviationbar.storage.parquet import TickStorage

logger = logging.getLogger(__name__)

# Default checkpoint directory
_CHECKPOINT_DIR = Path(user_cache_dir("opendeviationbar", "terrylica")) / "checkpoints"


def _get_population_config():  # noqa: ANN202
    """Lazy access to PopulationConfig (avoids circular import)."""
    from opendeviationbar.config import Settings

    return Settings.get().population


@dataclass
class PopulationContext:
    """Bundle of loop-constant state for cache population.

    Groups parameters that remain constant throughout the populate loop,
    reducing parameter passing from ~15 args to a single context object.
    """

    symbol: str
    threshold_decimal_bps: int
    ouroboros_mode: str
    include_microstructure: bool
    notify: bool
    inter_bar_lookback_count: int | None
    inter_bar_lookback_bars: int | None
    include_intra_bar_features: bool
    source_version: str
    storage: TickStorage
    thread_pool: ThreadPoolExecutor | None
    observer: ResumableObserver | None
    trace_id: str
    created_at: str
    start_date: str
    end_date: str
    market: str = "spot"


@dataclass
class PopulationCheckpoint:
    """Checkpoint for resumable cache population.

    Tracks progress of a multi-day cache population job, allowing
    resumption after interruption with bar-level accuracy.

    Issue #69 Enhancements:
    - processor_checkpoint: Preserves incomplete bar state
    - last_trade_timestamp_us: For mid-day resume filtering
    - include_microstructure: Track feature configuration
    - ouroboros_mode: Track reset mode for consistency

    Attributes
    ----------
    symbol : str
        Trading symbol (e.g., "BTCUSDT").
    threshold_bps : int
        Threshold in decimal basis points.
    start_date : str
        Original start date (YYYY-MM-DD).
    end_date : str
        Target end date (YYYY-MM-DD).
    last_completed_date : str
        Most recent successfully completed date.
    bars_written : int
        Total bars written so far.
    created_at : str
        ISO timestamp of checkpoint creation.
    updated_at : str
        ISO timestamp of last update.
    processor_checkpoint : dict | None
        Serialized processor state (incomplete bar, defer_open).
        Enables bar-level resumability (Issue #69).
    last_trade_timestamp_us : int | None
        Timestamp of last processed trade for mid-day resume.
    include_microstructure : bool
        Whether microstructure features are enabled.
    ouroboros_mode : str
        Ouroboros reset mode for consistency.
    """

    symbol: str
    threshold_bps: int
    start_date: str
    end_date: str
    last_completed_date: str
    bars_written: int = 0
    created_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
    updated_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
    # Issue #69: Bar-level resumability
    processor_checkpoint: dict | None = None
    last_trade_timestamp_us: int | None = None
    include_microstructure: bool = False
    ouroboros_mode: str = "aion"
    # Issue #72: Full Audit Trail - agg_trade_id range in incomplete bar
    first_agg_trade_id_in_bar: int | None = None
    last_agg_trade_id_in_bar: int | None = None
    # Issue #111 (Ariadne): High-water mark for deterministic resume via fromId
    last_processed_agg_trade_id: int | None = None
    # Issue #128: Feature computation config at checkpoint time
    compute_tier2: bool = True
    compute_tier3: bool = False
    compute_hurst: bool = False
    compute_permutation_entropy: bool = False

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return asdict(self)

    @classmethod
    def from_processor_state(
        cls,
        ctx: PopulationContext,
        date: str,
        total_bars: int,
        proc_cp: dict | None,
    ) -> PopulationCheckpoint:
        """Factory: construct checkpoint from current processor state.

        Consolidates the ~30-line checkpoint construction that was
        duplicated in the populate loop.
        """
        pop = _get_population_config()
        return cls(
            symbol=ctx.symbol,
            threshold_bps=ctx.threshold_decimal_bps,
            start_date=ctx.start_date,
            end_date=ctx.end_date,
            last_completed_date=date,
            bars_written=total_bars,
            created_at=ctx.created_at,
            include_microstructure=ctx.include_microstructure,
            ouroboros_mode=ctx.ouroboros_mode,
            processor_checkpoint=proc_cp,
            last_trade_timestamp_us=(
                proc_cp.get("last_timestamp_us") if proc_cp else None
            ),
            first_agg_trade_id_in_bar=(
                (proc_cp.get("incomplete_bar_raw") or {}).get("first_agg_trade_id")
                if proc_cp
                else None
            ),
            last_agg_trade_id_in_bar=(
                (proc_cp.get("incomplete_bar_raw") or {}).get("last_agg_trade_id")
                if proc_cp
                else None
            ),
            last_processed_agg_trade_id=(
                proc_cp.get("last_trade_id") if proc_cp else None
            ),
            compute_tier2=pop.compute_tier2,
            compute_tier3=pop.compute_tier3,
            compute_hurst=pop.compute_hurst,
            compute_permutation_entropy=pop.compute_permutation_entropy,
        )

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> PopulationCheckpoint:
        """Create from dictionary, ignoring unknown keys (e.g., provenance)."""
        import dataclasses

        field_names = {f.name for f in dataclasses.fields(cls)}
        filtered = {k: v for k, v in data.items() if k in field_names}
        return cls(**filtered)

    def save(self, path: Path) -> None:
        """Save checkpoint to file with atomic write.

        Uses tempfile + fsync + rename pattern for crash safety.

        Parameters
        ----------
        path : Path
            Path to save checkpoint file.
        """
        from .storage import _checkpoint_provenance

        self.updated_at = datetime.now(UTC).isoformat()
        d = self.to_dict()
        d["provenance"] = _checkpoint_provenance()
        data = json.dumps(d, indent=2)

        # Ensure parent directory exists
        path.parent.mkdir(parents=True, exist_ok=True)

        # Atomic write: write to temp file, fsync, rename
        fd, temp_path = tempfile.mkstemp(
            dir=path.parent,
            prefix=".checkpoint_",
            suffix=".tmp",
        )
        try:
            with os.fdopen(fd, "w") as f:
                f.write(data)
                f.flush()
                os.fsync(f.fileno())

            # Atomic rename (POSIX guarantees this is atomic)
            os.replace(temp_path, path)
            logger.debug("Saved checkpoint to %s", path)
        except (OSError, RuntimeError):
            # Clean up temp file on failure
            try:
                os.unlink(temp_path)
            except OSError:
                pass
            raise

    @classmethod
    def load(cls, path: Path) -> PopulationCheckpoint | None:
        """Load checkpoint from file.

        Parameters
        ----------
        path : Path
            Path to checkpoint file.

        Returns
        -------
        PopulationCheckpoint | None
            Loaded checkpoint, or None if file doesn't exist.
        """
        if not path.exists():
            return None

        try:
            data = json.loads(path.read_text())
            return cls.from_dict(data)
        except (json.JSONDecodeError, KeyError, TypeError) as e:
            logger.warning("Failed to load checkpoint from %s: %s", path, e)
            return None
