"""A tenant's time zone (Super Admin › Tenant › Settings): portal users see
every date and time in it, and exports print times in it.

Timestamps are stored in UTC; this only changes how they're shown. A plain
date ("2026-10-22") or a wall-clock time without an offset is left alone —
there's no instant to convert."""

import re
import uuid
from datetime import datetime, timezone
from functools import lru_cache
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

DEFAULT_TIMEZONE = "Asia/Calcutta"

# An ISO timestamp with its offset ("…T09:27:01.36+00:00", "…T09:27Z") —
# how a timestamp reads once it's in a JSON row.
_ISO_INSTANT = re.compile(r"^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}(:?\d{2})?)$")


@lru_cache(maxsize=1)
def timezone_names() -> frozenset[str]:
    return frozenset(available_timezones())


def listed_timezones() -> list[str]:
    """Every zone a tenant can be set to, A–Z ("Factory" is a placeholder,
    not a place)."""
    return sorted(n for n in timezone_names() if n != "Factory")


def check_timezone(name: str) -> str:
    """The name, if it's a time zone this server knows (IANA, e.g.
    "Asia/Calcutta", "Europe/London", "UTC"); ValueError otherwise."""
    name = (name or "").strip()
    if name not in timezone_names():
        raise ValueError(f'"{name}" isn\'t a known time zone — use an IANA name such as "Asia/Calcutta" or "Europe/London".')
    return name


def zone(name: str | None) -> ZoneInfo:
    try:
        return ZoneInfo(name or DEFAULT_TIMEZONE)
    except (ZoneInfoNotFoundError, ValueError):
        return ZoneInfo(DEFAULT_TIMEZONE)


async def tenant_zone(db: AsyncSession, tenant_id: uuid.UUID) -> ZoneInfo:
    from app.models.identity import Tenant

    name = (await db.execute(select(Tenant.timezone).where(Tenant.id == tenant_id))).scalar_one_or_none()
    return zone(name)


def localize(value: object, tz: ZoneInfo) -> object:
    """A timestamp (a datetime with a zone, or an ISO string with an offset)
    as a naive datetime in `tz`, to the second — what an export prints.
    Anything else comes back unchanged."""
    if isinstance(value, datetime):
        if value.tzinfo is None:
            return value
        return value.astimezone(tz).replace(tzinfo=None, microsecond=0)
    if isinstance(value, str) and _ISO_INSTANT.match(value):
        try:
            parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        except ValueError:
            return value
        return parsed.astimezone(tz).replace(tzinfo=None, microsecond=0)
    return value


def localize_rows(rows: list[dict], tz: ZoneInfo) -> list[dict]:
    return [{k: localize(v, tz) for k, v in row.items()} for row in rows]


def now_in(tz: ZoneInfo) -> datetime:
    return datetime.now(timezone.utc).astimezone(tz)
