"""
PUT /onboarding/addresses request/response schemas — PRD-HWONB-005.

One call, four `addressType` sub-objects: `dba`, `legal`, `mailing`,
`chargeback`. Only `dba` is always required with no `isSameAsDba` toggle
(§4). The other three default `isSameAsDba: true`; when true every other
field on that sub-object is ignored/omitted from the GP payload even if
populated (§3/AC-2) — that omission happens in
`services.py::_build_gp_addresses_payload`, not here; this schema only
validates what the merchant actually submitted.

Field rules exactly per PRD-HWONB-005 §1. `contactTitle` uses the
Addresses-specific 19-value enum (C-10) — distinct from the Owners screen's
short-code set (PRD-HWONB-007) — sourced from
`helpers.field_mapping.ADDRESS_CONTACT_TITLE_CHOICES` so the two screens
never accidentally share a constant.
"""

from __future__ import annotations

import re
from typing import Any, Dict, List, Optional

from pydantic import BaseModel, EmailStr, Field, field_validator, model_validator

from src.apps.merchant_onboarding.helpers.field_mapping import (
    ADDRESS_CONTACT_TITLE_CHOICES,
    COUNTRY_CODE_DEFAULT,
)

# §1 — addressLine1/2 allow letters, digits, spaces, and #&*'-, only.
_ADDRESS_LINE_RE = re.compile(r"^[A-Za-z0-9#&*'\-, ]{1,25}$")
# §1 — zipCode: exact 5 digits.
_ZIP_RE = re.compile(r"^\d{5}$")
# §1 — phoneNumber: 10 digits, area code's first digit must not be 0 or 1.
_PHONE_RE = re.compile(r"^[2-9]\d{9}$")
# §1 — phoneCountryCode: 2-7 digits (leading + optional). PRD's own example
# defaults this to "+1" — only 1 digit after the "+" — which is
# inconsistent with a strict "2-7 digits" reading of the digit count alone,
# so this counts total characters (including a leading "+") 2-7, not digits
# only, so the documented default validates.
_PHONE_COUNTRY_CODE_RE = re.compile(r"^\+?\d+$")
_PHONE_COUNTRY_CODE_MIN_LEN = 2
_PHONE_COUNTRY_CODE_MAX_LEN = 7

_REQUIRED_WHEN_NOT_SAME_AS_DBA = (
    "firstName",
    "lastName",
    "contactTitle",
    "email",
    "addressLine1",
    "city",
    "state",
    "country",
    "zipCode",
    "phoneNumber",
    "phoneCountryCode",
)


def _validate_contact_title(v: Optional[str]) -> Optional[str]:
    if v is not None and v not in ADDRESS_CONTACT_TITLE_CHOICES:
        raise ValueError(
            f"contactTitle must be one of {ADDRESS_CONTACT_TITLE_CHOICES} (Addresses' 19-value enum, C-10)"
        )
    return v


def _validate_address_line1(v: Optional[str]) -> Optional[str]:
    if v is not None and not _ADDRESS_LINE_RE.match(v):
        raise ValueError("addressLine1 must be 1-25 chars: letters, digits, spaces, or #&*'-,")
    return v


def _validate_address_line2(v: Optional[str]) -> Optional[str]:
    if v and not _ADDRESS_LINE_RE.match(v):
        raise ValueError("addressLine2 must be 1-25 chars: letters, digits, spaces, or #&*'-,")
    return v


def _validate_zip(v: Optional[str]) -> Optional[str]:
    if v is None:
        return v
    v = str(v)
    if not _ZIP_RE.match(v):
        raise ValueError("zipCode must be exactly 5 digits")
    return v


def _validate_phone(v: Optional[str]) -> Optional[str]:
    if v is None:
        return v
    v = str(v)
    if not _PHONE_RE.match(v):
        raise ValueError("phoneNumber must be 10 digits with area code's first digit not 0/1")
    return v


def _validate_phone_country_code(v: Optional[str]) -> Optional[str]:
    if v is None:
        return v
    v = str(v)
    if not _PHONE_COUNTRY_CODE_RE.match(v) or not (
        _PHONE_COUNTRY_CODE_MIN_LEN <= len(v) <= _PHONE_COUNTRY_CODE_MAX_LEN
    ):
        raise ValueError("phoneCountryCode must be 2-7 characters: digits, optionally prefixed with +")
    return v


class DbaAddress(BaseModel):
    """
    Fieldset 1 — "Business Address & Primary Contact". Always shown, always
    required, no `isSameAsDba` toggle (§4).
    """

    firstName: str = Field(..., min_length=1, max_length=25)
    lastName: str = Field(..., min_length=1, max_length=25)
    contactTitle: str
    email: EmailStr = Field(..., max_length=100)
    addressLine1: str = Field(..., min_length=1, max_length=25)
    addressLine2: Optional[str] = Field(None, max_length=25)
    city: str = Field(..., max_length=13)
    state: str = Field(..., min_length=2, max_length=2)
    country: str = Field(default=COUNTRY_CODE_DEFAULT, max_length=3)
    zipCode: str
    phoneNumber: str
    phoneCountryCode: str = Field(default="+1")

    _v_contact_title = field_validator("contactTitle")(_validate_contact_title)
    _v_line1 = field_validator("addressLine1")(_validate_address_line1)
    _v_line2 = field_validator("addressLine2")(_validate_address_line2)
    _v_zip = field_validator("zipCode")(_validate_zip)
    _v_phone = field_validator("phoneNumber")(_validate_phone)
    _v_phone_cc = field_validator("phoneCountryCode")(_validate_phone_country_code)


class ConditionalAddress(BaseModel):
    """
    Fieldsets 2-4 — Legal / Mailing / Chargeback. `isSameAsDba` defaults
    True (collapsed in the UI); when False, every field below becomes
    required (§4/AC-1/AC-2) — enforced in `_validate_required_fields` below,
    not via Pydantic's own required/optional field mechanism, since the
    requiredness is conditional on `isSameAsDba`.
    """

    isSameAsDba: bool = True
    firstName: Optional[str] = Field(None, min_length=1, max_length=25)
    lastName: Optional[str] = Field(None, min_length=1, max_length=25)
    contactTitle: Optional[str] = None
    email: Optional[EmailStr] = Field(None, max_length=100)
    addressLine1: Optional[str] = Field(None, min_length=1, max_length=25)
    addressLine2: Optional[str] = Field(None, max_length=25)
    city: Optional[str] = Field(None, max_length=13)
    state: Optional[str] = Field(None, min_length=2, max_length=2)
    country: Optional[str] = Field(default=COUNTRY_CODE_DEFAULT, max_length=3)
    zipCode: Optional[str] = None
    phoneNumber: Optional[str] = None
    phoneCountryCode: Optional[str] = "+1"

    _v_contact_title = field_validator("contactTitle")(_validate_contact_title)
    _v_line1 = field_validator("addressLine1")(_validate_address_line1)
    _v_line2 = field_validator("addressLine2")(_validate_address_line2)
    _v_zip = field_validator("zipCode")(_validate_zip)
    _v_phone = field_validator("phoneNumber")(_validate_phone)
    _v_phone_cc = field_validator("phoneCountryCode")(_validate_phone_country_code)

    @model_validator(mode="after")
    def _validate_required_fields(self) -> "ConditionalAddress":
        if self.isSameAsDba:
            return self
        missing = [
            name
            for name in _REQUIRED_WHEN_NOT_SAME_AS_DBA
            if getattr(self, name) in (None, "")
        ]
        if missing:
            raise ValueError(
                "When isSameAsDba is false, these fields are required: " + ", ".join(missing)
            )
        return self


class AddressesRequest(BaseModel):
    """`PUT /onboarding/addresses` request body — §3."""

    dba: DbaAddress
    legal: ConditionalAddress
    mailing: ConditionalAddress
    chargeback: ConditionalAddress


class AddressesResponse(BaseModel):
    """
    `PUT /onboarding/addresses` response — echoes the saved snapshot plus any
    GP field-level errors (AC-07 style, matching every other section's
    save-response shape). `tsys_errors` is None on a clean push.
    """

    dba: Dict[str, Any]
    legal: Dict[str, Any]
    mailing: Dict[str, Any]
    chargeback: Dict[str, Any]
    tsys_errors: Optional[List[Dict[str, str]]] = None
