from __future__ import annotations

import hashlib
import json
from dataclasses import asdict, dataclass
from datetime import date, datetime, time, timedelta
from typing import Any
from zoneinfo import ZoneInfo

import jdatetime

TEHRAN = ZoneInfo("Asia/Tehran")


class OutageDataError(ValueError):
    """Raised when an outage payload cannot be normalized safely."""


@dataclass(frozen=True, slots=True)
class Outage:
    outage_number: str
    outage_date: str
    start_time: str
    stop_time: str
    address: str
    reason: str
    is_planned: bool

    @classmethod
    def from_api(cls, payload: Any) -> Outage:
        if not isinstance(payload, dict):
            raise OutageDataError("outage entry must be an object")

        outage_number = _required_text(payload, "outage_number")
        outage_date = _required_text(payload, "outage_date")
        start_time = _required_text(payload, "outage_start_time")
        stop_time = _required_text(payload, "outage_stop_time")
        address = str(payload.get("outage_address") or payload.get("address") or "").strip()
        reason = str(payload.get("reason_outage") or "").strip()
        is_planned = payload.get("is_planned", True)
        if not isinstance(is_planned, bool):
            raise OutageDataError("is_planned must be a boolean")

        outage = cls(
            outage_number=outage_number,
            outage_date=outage_date,
            start_time=start_time,
            stop_time=stop_time,
            address=address,
            reason=reason,
            is_planned=is_planned,
        )
        outage.start_datetime()
        outage.stop_datetime()
        return outage

    @classmethod
    def from_dict(cls, payload: Any) -> Outage:
        if not isinstance(payload, dict):
            raise OutageDataError("stored outage snapshot must be an object")
        try:
            outage = cls(
                outage_number=str(payload["outage_number"]),
                outage_date=str(payload["outage_date"]),
                start_time=str(payload["start_time"]),
                stop_time=str(payload["stop_time"]),
                address=str(payload["address"]),
                reason=str(payload["reason"]),
                is_planned=payload["is_planned"],
            )
        except (KeyError, TypeError) as error:
            raise OutageDataError("stored outage snapshot has an invalid schema") from error
        if not isinstance(outage.is_planned, bool):
            raise OutageDataError("stored is_planned must be a boolean")
        outage.start_datetime()
        outage.stop_datetime()
        return outage

    def to_dict(self) -> dict[str, str | bool]:
        return asdict(self)

    def fingerprint(self) -> str:
        serialized = json.dumps(
            self.to_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
        )
        return hashlib.sha256(serialized.encode("utf-8")).hexdigest()

    def start_datetime(self) -> datetime:
        return _jalali_datetime(self.outage_date, self.start_time)

    def stop_datetime(self) -> datetime:
        start = self.start_datetime()
        stop = _jalali_datetime(self.outage_date, self.stop_time)
        if stop <= start:
            stop += timedelta(days=1)
        return stop


def jalali_query_window(now: datetime, lookahead_days: int) -> tuple[str, str]:
    local_now = now.astimezone(TEHRAN)
    start = jdatetime.date.fromgregorian(date=local_now.date())
    end = jdatetime.date.fromgregorian(date=local_now.date() + timedelta(days=lookahead_days))
    return start.strftime("%Y/%m/%d"), end.strftime("%Y/%m/%d")


def tehran_now() -> datetime:
    return datetime.now(TEHRAN)


def _required_text(payload: dict[str, Any], field: str) -> str:
    value = payload.get(field)
    if value is None or not str(value).strip():
        raise OutageDataError(f"{field} is required")
    return str(value).strip()


def _jalali_datetime(date_text: str, time_text: str) -> datetime:
    try:
        year, month, day = (int(part) for part in date_text.split("/"))
        hour, minute = (int(part) for part in time_text.split(":"))
        gregorian_date: date = jdatetime.date(year, month, day).togregorian()
        parsed_time = time(hour, minute)
    except (TypeError, ValueError) as error:
        raise OutageDataError(f"invalid Jalali date/time: {date_text} {time_text}") from error
    return datetime.combine(gregorian_date, parsed_time, tzinfo=TEHRAN)
