"""Immutable normalization inputs and outputs."""

from __future__ import annotations

import re
from dataclasses import dataclass

from ..domain.models import RegionScopedIdentity
from ..errors import ContractViolation

_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$")


def _optional_text(value: object, field_name: str) -> str | None:
    if value is None:
        return None
    if not isinstance(value, str):
        raise ContractViolation(f"{field_name} must be a string or null")
    normalized = value.strip()
    return normalized or None


@dataclass(frozen=True, slots=True)
class NormalizationContext:
    """Explicit country and phone context; no network lookup is permitted."""

    country_code: str | None = None
    calling_code: str | None = None
    national_trunk_prefix: str = "0"

    def __post_init__(self) -> None:
        country = _optional_text(self.country_code, "country_code")
        if country is not None:
            country = country.upper()
            if not re.fullmatch(r"[A-Z]{2}", country):
                raise ContractViolation("country_code must be an ISO alpha-2 value")
        calling = _optional_text(self.calling_code, "calling_code")
        if calling is not None:
            calling = calling.removeprefix("+")
            if not calling.isdigit() or not 1 <= len(calling) <= 4:
                raise ContractViolation("calling_code must contain 1 to 4 digits")
        trunk = _optional_text(self.national_trunk_prefix, "national_trunk_prefix")
        if trunk is None or not trunk.isdigit() or len(trunk) > 4:
            raise ContractViolation("national_trunk_prefix must contain 1 to 4 digits")
        object.__setattr__(self, "country_code", country)
        object.__setattr__(self, "calling_code", calling)
        object.__setattr__(self, "national_trunk_prefix", trunk)


@dataclass(frozen=True, slots=True)
class RawBusinessFields:
    """Detached raw business fields extracted from one parsed OSM object."""

    identity: RegionScopedIdentity
    name: str | None = None
    website: str | None = None
    phone: str | None = None
    address: str | None = None
    city: str | None = None
    state: str | None = None
    postcode: str | None = None
    country: str | None = None
    longitude_e7: int | None = None
    latitude_e7: int | None = None
    source_tags_hash: str = ""

    def __post_init__(self) -> None:
        if not isinstance(self.identity, RegionScopedIdentity):
            raise ContractViolation("identity must be RegionScopedIdentity")
        for field_name in (
            "name",
            "website",
            "phone",
            "address",
            "city",
            "state",
            "postcode",
            "country",
        ):
            object.__setattr__(
                self, field_name, _optional_text(getattr(self, field_name), field_name)
            )
        for field_name in ("longitude_e7", "latitude_e7"):
            value = getattr(self, field_name)
            if value is not None and (isinstance(value, bool) or not isinstance(value, int)):
                raise ContractViolation(f"{field_name} must be an integer or null")
        if (
            self.longitude_e7 is not None
            and not -1_800_000_000 <= self.longitude_e7 <= 1_800_000_000
        ):
            raise ContractViolation("longitude_e7 is outside WGS84 bounds")
        if self.latitude_e7 is not None and not -900_000_000 <= self.latitude_e7 <= 900_000_000:
            raise ContractViolation("latitude_e7 is outside WGS84 bounds")
        if not _SHA256_PATTERN.fullmatch(self.source_tags_hash):
            raise ContractViolation("source_tags_hash must be lowercase SHA-256")


@dataclass(frozen=True, slots=True)
class NormalizedBusiness:
    """Canonical business fields used only for deterministic comparison."""

    identity: RegionScopedIdentity
    name: str | None
    domain: str | None
    phone: str | None
    address: str | None
    city: str | None
    state: str | None
    postcode: str | None
    country: str | None
    longitude_e7: int | None
    latitude_e7: int | None
    source_tags_hash: str
    identity_hash: str

    def __post_init__(self) -> None:
        if not isinstance(self.identity, RegionScopedIdentity):
            raise ContractViolation("identity must be RegionScopedIdentity")
        for field_name in (
            "name",
            "domain",
            "phone",
            "address",
            "city",
            "state",
            "postcode",
            "country",
        ):
            value = getattr(self, field_name)
            if value is not None and (not isinstance(value, str) or not value):
                raise ContractViolation(f"{field_name} must be nonblank or null")
        if self.country is not None and not re.fullmatch(r"[A-Z]{2}", self.country):
            raise ContractViolation("country must be an uppercase ISO alpha-2 shape or null")
        if self.phone is not None and not re.fullmatch(r"\+?[0-9]{7,15}", self.phone):
            raise ContractViolation("phone must be a normalized digit sequence or null")
        for field_name in ("longitude_e7", "latitude_e7"):
            value = getattr(self, field_name)
            if value is not None and (isinstance(value, bool) or not isinstance(value, int)):
                raise ContractViolation(f"{field_name} must be an integer or null")
        if (
            self.longitude_e7 is not None
            and not -1_800_000_000 <= self.longitude_e7 <= 1_800_000_000
        ):
            raise ContractViolation("longitude_e7 is outside WGS84 bounds")
        if self.latitude_e7 is not None and not -900_000_000 <= self.latitude_e7 <= 900_000_000:
            raise ContractViolation("latitude_e7 is outside WGS84 bounds")
        if not _SHA256_PATTERN.fullmatch(self.source_tags_hash):
            raise ContractViolation("source_tags_hash must be lowercase SHA-256")
        if not _SHA256_PATTERN.fullmatch(self.identity_hash):
            raise ContractViolation("identity_hash must be lowercase SHA-256")

    @property
    def comparable_field_count(self) -> int:
        return sum(
            value is not None
            for value in (
                self.name,
                self.domain,
                self.phone,
                self.address,
                self.city,
                self.postcode,
            )
        )
