from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Mapping

from flownet.core.exceptions import FlowDefinitionError


@dataclass(frozen=True)
class ReplayCheckpoint:
    request_id: str
    stage_id: str
    epoch: int
    plan_id: str | None = None
    replay_attempt: int = 0
    replay_trigger: str | None = None
    checkpoint_epoch: int | None = None

    @property
    def is_replay(self) -> bool:
        return self.replay_attempt > 0

    def sink_idempotency_key(self) -> str:
        plan_part = self.plan_id or "-"
        return f"{self.request_id}:{self.stage_id}:{self.epoch}:{plan_part}"

    def sink_idempotency_payload(
        self,
        *,
        row_fingerprint: str | None = None,
    ) -> dict[str, Any]:
        payload: dict[str, Any] = {
            "version": "v1",
            "sink_idempotency_key": self.sink_idempotency_key(),
            "request_id": self.request_id,
            "stage_id": self.stage_id,
            "epoch": self.epoch,
            "plan_id": self.plan_id,
            "replay_attempt": self.replay_attempt,
            "checkpoint_epoch": self.checkpoint_epoch,
            "replay_trigger": self.replay_trigger,
        }
        if row_fingerprint is not None:
            fingerprint = str(row_fingerprint)
            payload["row_fingerprint"] = fingerprint
            payload["row_dedupe_key"] = f"{payload['sink_idempotency_key']}:{fingerprint}"
        return payload


def parse_replay_checkpoint(
    meta: Mapping[str, Any] | None,
    *,
    request_id: str,
    stage_id: str,
) -> ReplayCheckpoint:
    safe_meta = dict(meta or {})
    sys_meta = safe_meta.get("sys")
    if sys_meta is None:
        sys_meta = {}
    if not isinstance(sys_meta, dict):
        raise FlowDefinitionError("sys must be a dict when provided.")

    epoch = _parse_non_negative_int(sys_meta.get("epoch"), field="sys.epoch", default=1)
    sys_stage_id = sys_meta.get("stage_id")
    if sys_stage_id is not None and str(sys_stage_id) != str(stage_id):
        raise FlowDefinitionError("sys.stage_id must match the active stage_id.")
    plan_id = _extract_plan_id(sys_meta)

    replay_attempt = 0
    replay_trigger: str | None = None
    checkpoint_epoch: int | None = None
    replay_meta = sys_meta.get("replay")
    if replay_meta is not None:
        if not isinstance(replay_meta, dict):
            raise FlowDefinitionError("sys.replay must be a dict when provided.")
        replay_attempt = _parse_non_negative_int(
            replay_meta.get("attempt"),
            field="sys.replay.attempt",
            default=1,
        )
        replay_trigger_value = replay_meta.get("trigger")
        if replay_trigger_value is not None:
            replay_trigger = str(replay_trigger_value)
        checkpoint_epoch_value = replay_meta.get("checkpoint_epoch")
        if checkpoint_epoch_value is None:
            raise FlowDefinitionError(
                "sys.replay.checkpoint_epoch is required when sys.replay is provided."
            )
        checkpoint_epoch = _parse_non_negative_int(
            checkpoint_epoch_value,
            field="sys.replay.checkpoint_epoch",
        )
        if replay_attempt > 0 and checkpoint_epoch is not None and checkpoint_epoch >= epoch:
            raise FlowDefinitionError(
                "sys.replay.checkpoint_epoch must be < sys.epoch for replay events."
            )

    return ReplayCheckpoint(
        request_id=request_id,
        stage_id=str(stage_id),
        epoch=epoch,
        plan_id=plan_id,
        replay_attempt=replay_attempt,
        replay_trigger=replay_trigger,
        checkpoint_epoch=checkpoint_epoch,
    )


def _extract_plan_id(sys_meta: Mapping[str, Any]) -> str | None:
    raw_plan_id = sys_meta.get("plan_id")
    if raw_plan_id is not None:
        return str(raw_plan_id)
    plan = sys_meta.get("plan")
    if isinstance(plan, Mapping):
        plan_id = plan.get("plan_id")
        if plan_id is not None:
            return str(plan_id)
    return None


def _parse_non_negative_int(value: Any, *, field: str, default: int | None = None) -> int:
    if value is None:
        if default is None:
            raise FlowDefinitionError(f"{field} must be an integer >= 0.")
        return default
    try:
        parsed = int(value)
    except (TypeError, ValueError) as exc:
        raise FlowDefinitionError(f"{field} must be an integer >= 0.") from exc
    if parsed < 0:
        raise FlowDefinitionError(f"{field} must be an integer >= 0.")
    return parsed


__all__ = ["ReplayCheckpoint", "parse_replay_checkpoint"]
