"""
PUT /onboarding/business request/response schemas — PRD-HWONB-003 §1.

This mirrors the GP `POST /basecrm/applications/{appId}/business` contract
near-verbatim (PRD-HWONB-003 §3: "no reshaping needed, unlike Products/Fees"),
so `BusinessInformationRequest.to_gp_payload()` is (almost) a straight
`model_dump(exclude_none=True)` — the only extra work is the couple of
conditional-omission rules called out in §1/§4 (yearsInBusiness vs
plannedOpenDate, chargeback email-vs-fax).

Enum choices live in `helpers/field_mapping.py` (this section's dedicated
block) rather than inline here, per PRD-HWONB-001 §13.1's "one place to grep
every GP-is-weird-about-X constant" convention.
"""

from __future__ import annotations

import re
from datetime import date, datetime
from typing import List, Optional

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

from src.apps.merchant_onboarding.helpers.field_mapping import (
    BUSINESS_TYPE_CHOICES,
    CHARITY_EXEMPT_SIC_CODES,
    OWNERSHIP_TYPE_CHOICES,
    TAX_ID_TYPE_CHOICES,
)

# ---------------------------------------------------------------------------
# Regex / format helpers (PRD-HWONB-003 §1)
# ---------------------------------------------------------------------------

# dbaName / legalName — 1-23 chars, "#&*-'" plus spaces (and alnum, implied by
# every real example — the PDF's charset row only lists the *punctuation*
# allowed on top of the obvious alnum set).
_NAME_RE = re.compile(r"^[A-Za-z0-9 #&*\-']{1,23}$")

# businessDescription — 1-25 chars, `!@#$%*()_-="/'.,?` punctuation set.
_BUSINESS_DESCRIPTION_RE = re.compile(r"""^[A-Za-z0-9 !@#$%*()_\-="/'.,?]{1,25}$""")

_SIC_CODE_RE = re.compile(r"^\d{4}$")

# websiteAddress — PRD's own "URL-ish regex" description; kept permissive.
_WEBSITE_RE = re.compile(r"^(https?://|www\.)\S+$", re.IGNORECASE)

_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

# taxId — PRD §1.3 explicitly says: ignore the PDF's internally-inconsistent
# regex; validate against the stated length (9 digits) + standard EIN/SSN/ITIN
# format instead. Accept with-or-without the conventional dash.
_EIN_RE = re.compile(r"^\d{2}-?\d{7}$")
_SSN_ITIN_RE = re.compile(r"^\d{3}-?\d{2}-?\d{4}$")

_PHONE_COUNTRY_CODE_RE = re.compile(r"^\+\d{1,3}$")

_MONTH_FIELDS = (
    "janFlag", "febFlag", "marFlag", "aprFlag", "mayFlag", "junFlag",
    "julFlag", "augFlag", "sepFlag", "octFlag", "novFlag", "decFlag",
)


def _parse_flexible_date(value: str, field_name: str) -> date:
    """Accept both `MM/DD/YYYY` and ISO `YYYY-MM-DD` on input (a native
    `<input type="date">` submits ISO; older callers may send MM/DD/YYYY).
    Output is always normalized to ISO `YYYY-MM-DD`, which is what the live GP
    CERT API accepts on the wire (the PDF's MM/DD/YYYY spec is stale)."""
    for fmt in ("%m/%d/%Y", "%Y-%m-%d"):
        try:
            return datetime.strptime(value, fmt).date()
        except ValueError:
            continue
    raise ValueError(f"{field_name} must be a valid date (MM/DD/YYYY or YYYY-MM-DD)")


# ---------------------------------------------------------------------------
# Nested objects
# ---------------------------------------------------------------------------


class PhoneNumberBlock(BaseModel):
    """`merchCustServiceNum` — PRD-HWONB-003 §1.1. Field NAME is `merchCustServiceNum`
    (the PDF's own field table prints `merchCustServicEnum` — a typo; every real
    request/response body uses the spelling used here)."""

    phoneCountryCode: str = Field(..., description="e.g. '+1'")
    phoneNumber: int = Field(..., description="10-digit phone number, no country code")

    @field_validator("phoneCountryCode")
    @classmethod
    def _validate_country_code(cls, v: str) -> str:
        if not _PHONE_COUNTRY_CODE_RE.match(v):
            raise ValueError("phoneCountryCode must look like '+1'")
        return v

    @field_validator("phoneNumber")
    @classmethod
    def _validate_phone_number(cls, v: int) -> int:
        if not (1_000_000_000 <= v <= 9_999_999_999):
            raise ValueError("phoneNumber must be exactly 10 digits")
        return v


class ChargebackNotificationContact(BaseModel):
    """CM: send EITHER `email` OR the fax pair, never both, never neither
    (PRD-HWONB-003 §1.1/§4/AC-4). `faxNumber`/`faxCountryCode` are the
    §5 "missing from prototype" addition."""

    email: Optional[str] = None
    faxNumber: Optional[str] = None
    faxCountryCode: Optional[str] = None

    @model_validator(mode="after")
    def _email_xor_fax(self) -> "ChargebackNotificationContact":
        has_email = bool(self.email)
        has_fax = bool(self.faxNumber)
        if has_email and has_fax:
            raise ValueError(
                "chargebackNotificationContact: provide either email or fax, not both"
            )
        if not has_email and not has_fax:
            raise ValueError(
                "chargebackNotificationContact: one of email or fax is required"
            )
        if has_email and not _EMAIL_RE.match(self.email):
            raise ValueError("chargebackNotificationContact.email is not a valid email")
        if has_fax and not self.faxCountryCode:
            raise ValueError(
                "chargebackNotificationContact.faxCountryCode is required together with faxNumber"
            )
        return self


class BusinessDetails(BaseModel):
    """PRD-HWONB-003 §1.1."""

    dbaName: str
    legalName: str
    ownershipType: str
    businessType: str
    sicCode: str
    businessDescription: str
    isExistingBusiness: bool
    plannedOpenDate: Optional[str] = None
    yearsInBusiness: Optional[int] = None
    websiteAddress: Optional[str] = None
    charity501c3Exempt: Optional[bool] = None
    optOutFromElectronicStatement: bool = False
    merchCustServiceNum: PhoneNumberBlock
    chargebackNotificationContact: ChargebackNotificationContact

    @field_validator("dbaName", "legalName")
    @classmethod
    def _validate_name(cls, v: str) -> str:
        if not _NAME_RE.match(v):
            raise ValueError("must be 1-23 chars, letters/digits/spaces/# & * - '")
        return v

    @field_validator("businessDescription")
    @classmethod
    def _validate_description(cls, v: str) -> str:
        if not _BUSINESS_DESCRIPTION_RE.match(v):
            raise ValueError("businessDescription must be 1-25 chars of the allowed charset")
        return v

    @field_validator("ownershipType")
    @classmethod
    def _validate_ownership_type(cls, v: str) -> str:
        if v not in OWNERSHIP_TYPE_CHOICES:
            raise ValueError(f"ownershipType must be one of {OWNERSHIP_TYPE_CHOICES}")
        return v

    @field_validator("businessType")
    @classmethod
    def _validate_business_type(cls, v: str) -> str:
        if v not in BUSINESS_TYPE_CHOICES:
            raise ValueError(f"businessType must be one of {BUSINESS_TYPE_CHOICES}")
        return v

    @field_validator("sicCode")
    @classmethod
    def _validate_sic_code(cls, v: str) -> str:
        if not _SIC_CODE_RE.match(v):
            raise ValueError("sicCode must be exactly 4 numeric digits")
        return v

    @field_validator("websiteAddress")
    @classmethod
    def _validate_website(cls, v: Optional[str]) -> Optional[str]:
        if v is None:
            return v
        if len(v) > 100:
            raise ValueError("websiteAddress must be <= 100 chars")
        if not _WEBSITE_RE.match(v):
            raise ValueError("websiteAddress must look like a URL (http(s):// or www.)")
        return v

    @field_validator("yearsInBusiness")
    @classmethod
    def _validate_years_in_business(cls, v: Optional[int]) -> Optional[int]:
        if v is not None and not (1 <= v <= 999):
            raise ValueError("yearsInBusiness must be between 1 and 999")
        return v

    @model_validator(mode="after")
    def _validate_existing_business_pair(self) -> "BusinessDetails":
        """AC-2 — `yearsInBusiness` must NEVER be sent when `isExistingBusiness`
        is false; `plannedOpenDate` is required (future date) exactly when it's
        false, and must be omitted when true."""
        if self.isExistingBusiness:
            if self.plannedOpenDate is not None:
                raise ValueError(
                    "plannedOpenDate must be omitted when isExistingBusiness=true"
                )
            if self.yearsInBusiness is None:
                raise ValueError(
                    "yearsInBusiness is required when isExistingBusiness=true"
                )
        else:
            if self.yearsInBusiness is not None:
                raise ValueError(
                    "yearsInBusiness must NOT be sent when isExistingBusiness=false (AC-2)"
                )
            if self.plannedOpenDate is None:
                raise ValueError(
                    "plannedOpenDate is required when isExistingBusiness=false"
                )
            planned = _parse_flexible_date(self.plannedOpenDate, "plannedOpenDate")
            if planned <= date.today():
                raise ValueError("plannedOpenDate must be a future date")
            # Normalize to GP's required YYYY-MM-DD wire format regardless of
            # which format the client submitted (see _parse_flexible_date).
            # NB: the PDF dev guide V1.9 says MM/DD/YYYY, but the live CERT API
            # rejects that and GP's own Postman collection uses ISO — ISO is
            # the format the API actually accepts.
            self.plannedOpenDate = planned.strftime("%Y-%m-%d")
        return self

    @model_validator(mode="after")
    def _validate_charity_lock(self) -> "BusinessDetails":
        """AC-3 — sicCode 8398/8661 auto-checks (and locks) charity501c3Exempt=true.
        Server-side: default it to True if omitted, reject an explicit False as a
        client/state bug rather than silently overriding the merchant's input."""
        if self.sicCode in CHARITY_EXEMPT_SIC_CODES:
            if self.charity501c3Exempt is False:
                raise ValueError(
                    f"charity501c3Exempt must be true when sicCode is {self.sicCode!r} (AC-3)"
                )
            self.charity501c3Exempt = True
        return self


class AdditionalInfo(BaseModel):
    """PRD-HWONB-003 §1.2."""

    janFlag: bool
    febFlag: bool
    marFlag: bool
    aprFlag: bool
    mayFlag: bool
    junFlag: bool
    julFlag: bool
    augFlag: bool
    sepFlag: bool
    octFlag: bool
    novFlag: bool
    decFlag: bool

    bankruptcyBusiness: bool = False
    bankruptcyPersonal: bool = False
    bankruptcyBusinessFilingDate: Optional[str] = None
    bankruptcyPersonalFilingDate: Optional[str] = None

    # §5.2 — checkbox-gated free text (replaces the prototype's plain boolean).
    contractorsCardDataAccess: Optional[str] = None

    # §5.3 — new field. str, not int — GP's affiliatedMerchantId is a
    # zero-padded digit string; an int type silently drops leading zeros.
    affiliatedMerchantId: Optional[str] = None

    @field_validator("contractorsCardDataAccess")
    @classmethod
    def _validate_contractors_text(cls, v: Optional[str]) -> Optional[str]:
        if v is None:
            return v
        v = v.strip()
        if not (1 <= len(v) <= 250):
            raise ValueError("contractorsCardDataAccess must be 1-250 chars when provided")
        return v

    @field_validator("affiliatedMerchantId")
    @classmethod
    def _validate_affiliated_merchant_id(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and not (v.isdigit() and 1 <= len(v) <= 16 and int(v) >= 1):
            raise ValueError("affiliatedMerchantId must be a positive integer up to 16 digits")
        return v

    @model_validator(mode="after")
    def _validate_bankruptcy_dates(self) -> "AdditionalInfo":
        if self.bankruptcyBusiness and not self.bankruptcyBusinessFilingDate:
            raise ValueError(
                "bankruptcyBusinessFilingDate is required when bankruptcyBusiness=true"
            )
        if not self.bankruptcyBusiness and self.bankruptcyBusinessFilingDate:
            raise ValueError(
                "bankruptcyBusinessFilingDate must be omitted when bankruptcyBusiness=false"
            )
        if self.bankruptcyPersonal and not self.bankruptcyPersonalFilingDate:
            raise ValueError(
                "bankruptcyPersonalFilingDate is required when bankruptcyPersonal=true"
            )
        if not self.bankruptcyPersonal and self.bankruptcyPersonalFilingDate:
            raise ValueError(
                "bankruptcyPersonalFilingDate must be omitted when bankruptcyPersonal=false"
            )
        for field_name in ("bankruptcyBusinessFilingDate", "bankruptcyPersonalFilingDate"):
            raw = getattr(self, field_name)
            if raw:
                parsed = _parse_flexible_date(raw, field_name)
                if parsed >= date.today():
                    raise ValueError(f"{field_name} must be a past date")
                # These fields, like plannedOpenDate, go on the wire as
                # YYYY-MM-DD (the format the live GP CERT API accepts).
                setattr(self, field_name, parsed.strftime("%Y-%m-%d"))
        return self


class ApplicationIrsDetails(BaseModel):
    """PRD-HWONB-003 §1.3."""

    taxExempt: bool
    taxIdType: str
    taxId: str

    @field_validator("taxIdType")
    @classmethod
    def _validate_tax_id_type(cls, v: str) -> str:
        if v not in TAX_ID_TYPE_CHOICES:
            raise ValueError(f"taxIdType must be one of {TAX_ID_TYPE_CHOICES}")
        return v

    @model_validator(mode="after")
    def _validate_tax_id(self) -> "ApplicationIrsDetails":
        raw = self.taxId
        if self.taxIdType == "EIN":
            if not _EIN_RE.match(raw):
                raise ValueError("taxId must be a 9-digit EIN (##-####### or #########)")
        else:  # ITIN / SSN — same 9-digit shape
            if not _SSN_ITIN_RE.match(raw):
                raise ValueError("taxId must be a 9-digit SSN/ITIN")
        digits_only = re.sub(r"\D", "", raw)
        if len(digits_only) != 9:
            raise ValueError("taxId must contain exactly 9 digits")
        return self


class TradeReferenceContact(BaseModel):
    """Same wire convention as `PhoneNumberBlock` above — GP expects
    phoneCountryCode as a '+1'-style string, not a bare int, and requires
    phoneNumber whenever the contact block is present."""

    phoneCountryCode: str = Field(..., description="e.g. '+1'")
    phoneNumber: int = Field(..., description="10-12 digit phone number, no country code")

    @field_validator("phoneCountryCode")
    @classmethod
    def _validate_country_code(cls, v: str) -> str:
        if not _PHONE_COUNTRY_CODE_RE.match(v):
            raise ValueError("contact.phoneCountryCode must look like '+1'")
        return v

    @field_validator("phoneNumber")
    @classmethod
    def _validate_phone_number(cls, v: int) -> int:
        digits = len(str(abs(v)))
        if not (10 <= digits <= 12):
            raise ValueError("contact.phoneNumber must be 10-12 digits")
        return v


class TradeReference(BaseModel):
    """PRD-HWONB-003 §1.4 — the whole section is optional, but if any field is
    used, `contact.phoneCountryCode` becomes mandatory ("M (if trade reference
    section used)")."""

    tradeName: Optional[str] = None
    productSold: Optional[str] = None
    accountNumber: Optional[str] = None
    contact: Optional[TradeReferenceContact] = None

    @field_validator("tradeName", "productSold")
    @classmethod
    def _validate_len_25(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and not (1 <= len(v) <= 25):
            raise ValueError("must be 1-25 chars")
        return v

    @field_validator("accountNumber")
    @classmethod
    def _validate_account_number(cls, v: Optional[str]) -> Optional[str]:
        if v is None:
            return v
        if not (1 <= len(v) <= 17) or not v.isdigit():
            raise ValueError("accountNumber must be 1-17 numeric chars")
        return v

    @model_validator(mode="after")
    def _validate_section_used(self) -> "TradeReference":
        any_field_used = any(
            [self.tradeName, self.productSold, self.accountNumber, self.contact]
        )
        if any_field_used and self.contact is None:
            raise ValueError(
                "tradeReference.contact.phoneCountryCode is required once any "
                "trade reference field is provided"
            )
        return self


# ---------------------------------------------------------------------------
# Top-level request/response
# ---------------------------------------------------------------------------


class BusinessInformationRequest(BaseModel):
    """`PUT /onboarding/business` request body — PRD-HWONB-003 §1/§3.

    Shape mirrors the GP contract 1:1 (`businessDetails`, `additionalInfo`,
    `applicationIrsDetails`, `tradeReference`) — no HubWallet-side reshaping.
    """

    model_config = ConfigDict(str_strip_whitespace=True)

    businessDetails: BusinessDetails
    additionalInfo: AdditionalInfo
    applicationIrsDetails: ApplicationIrsDetails
    tradeReference: Optional[TradeReference] = None

    def to_gp_payload(self) -> dict:
        """Build the exact JSON body sent to GP — recursive `exclude_none`
        drops `plannedOpenDate`/`yearsInBusiness` (whichever doesn't apply)
        and the unused chargeback email/fax fields automatically, since the
        validators above guarantee only one side of each pair is populated."""
        return self.model_dump(exclude_none=True)


class BusinessInformationSaveResponse(BaseModel):
    """PRD-HWONB-003 §3 — `{ saved, tsys_errors, section_state, status }`."""

    saved: bool
    tsys_errors: Optional[List[dict]] = None
    section_state: dict
    status: str
