"""
PUT /onboarding/processing request/response schemas — PRD-HWONB-004 §1.

Mirrors the GP `POST /basecrm/applications/{appId}/processingInformation`
contract near-1:1 (three nested objects: `applicationProcessing`,
`applicationPatriotAct`, `applicationMerchantSurvey` — PRD-HWONB-004 §3).

Enum choices live in `helpers/field_mapping.py` (PRD-HWONB-004 block).

Two validations are intentionally NOT enforced here because they are
cross-section (need `businessDetails.ownershipType` from the already-saved
Business section, which this schema has no access to):
  - `governmentIdType` required iff ownershipType == "GE"
  - `businessIdType` required iff ownershipType != "GE"
These are enforced in `services.py::save_section` against the application's
persisted `business_data`, which N-2's sequencing gate guarantees exists
before a Processing save is ever attempted.
"""

from __future__ import annotations

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

from pydantic import BaseModel, ConfigDict, field_validator, model_validator

from src.apps.merchant_onboarding.helpers.field_mapping import (
    BUSINESS_ID_TYPE_CHOICES,
    BUSINESS_ID_TYPE_REQUIRES_PLACE_OF_ISSUANCE,
    BUSINESS_ID_TYPE_REQUIRES_DATE_ISSUED,
    CARD_TYPES_ACCEPTED_CHOICES,
    GOVERNMENT_ID_TYPE_CHOICES,
    MAX_ADDITIONAL_AFFILIATED_MERCHANTS,
    PREPAYMENT_TYPE_CHOICES,
)


def _parse_date_field(value: str, field_name: str) -> date:
    """Accept both ISO (`YYYY-MM-DD`) and `MM/DD/YYYY` — the PRD doesn't pin
    an exact wire format for these two date fields the way it does for
    Business's `MM/DD/YYYY` fields, so both are accepted defensively."""
    for fmt in ("%Y-%m-%d", "%m/%d/%Y"):
        try:
            return datetime.strptime(value, fmt).date()
        except ValueError:
            continue
    raise ValueError(f"{field_name} must be a valid date (YYYY-MM-DD or MM/DD/YYYY)")


# ---------------------------------------------------------------------------
# applicationProcessing
# ---------------------------------------------------------------------------


class AffiliatedMerchantEntry(BaseModel):
    """One row of `additionalAffiliatedMerchantId[]` — PRD-HWONB-004 §1.1/§5.2."""

    # str, not int — GP's affiliatedMerchantId is a zero-padded digit string
    # (e.g. "0000962025100013"); an int type silently drops leading zeros.
    affiliatedMerchantId: str

    @field_validator("affiliatedMerchantId")
    @classmethod
    def _validate_id(cls, v: str) -> str:
        if 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


class ApplicationProcessing(BaseModel):
    averageTicket: float
    highestTicket: float
    monthlyVolume: float
    businessToBusiness: int
    numberOfLocation: int
    cardTypesAccepted: str
    cardPresent: int
    eCommerce: int
    manuallyKeyedMoto: int
    webSiteAddress: Optional[str] = None
    shippingLeadTimeDays: Optional[int] = None
    billPriorToShip: bool
    prepaymentType: Optional[str] = None
    paymentCollected: Optional[str] = None
    contractorsCardDataAccess: Optional[str] = None
    affiliatedMerchantId: Optional[str] = None
    additionalAffiliatedMerchantId: Optional[List[AffiliatedMerchantEntry]] = None

    @field_validator("averageTicket", "highestTicket", "monthlyVolume")
    @classmethod
    def _validate_money(cls, v: float) -> float:
        # <=12 integer digits + 2 decimals.
        as_str = f"{v:.2f}"
        integer_part = as_str.split(".")[0].lstrip("-")
        if len(integer_part) > 12:
            raise ValueError("value exceeds 12 integer digits")
        if v < 0:
            raise ValueError("value must be >= 0")
        return round(v, 2)

    @field_validator("numberOfLocation")
    @classmethod
    def _validate_number_of_location(cls, v: int) -> int:
        # No formal PDF upper bound documented (§1.1/§5.5) — plain positive
        # integer client-side; tighten if CERT rejects an edge case.
        if v < 1:
            raise ValueError("numberOfLocation must be a positive integer")
        return v

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

    @field_validator("businessToBusiness", "cardPresent", "eCommerce", "manuallyKeyedMoto")
    @classmethod
    def _validate_pct_required(cls, v: int) -> int:
        if not (0 <= v <= 100):
            raise ValueError("must be 0-100")
        return v

    @field_validator("webSiteAddress")
    @classmethod
    def _validate_website(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and len(v) > 100:
            raise ValueError("webSiteAddress must be <= 100 chars")
        return v

    @field_validator("shippingLeadTimeDays")
    @classmethod
    def _validate_shipping_lead_time(cls, v: Optional[int]) -> Optional[int]:
        # C-08 (CERT-confirmed): any 1-99, not bucket-only.
        if v is not None and not (1 <= v <= 99):
            raise ValueError("shippingLeadTimeDays must be between 1 and 99")
        return v

    @field_validator("prepaymentType")
    @classmethod
    def _validate_prepayment_type(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and v not in PREPAYMENT_TYPE_CHOICES:
            raise ValueError(f"prepaymentType must be one of {PREPAYMENT_TYPE_CHOICES}")
        return v

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

    @field_validator("contractorsCardDataAccess")
    @classmethod
    def _validate_contractors_text(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and not (1 <= len(v.strip()) <= 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

    @field_validator("additionalAffiliatedMerchantId")
    @classmethod
    def _validate_additional_affiliated_cap(
        cls, v: Optional[List[AffiliatedMerchantEntry]]
    ) -> Optional[List[AffiliatedMerchantEntry]]:
        if v is not None and len(v) > MAX_ADDITIONAL_AFFILIATED_MERCHANTS:
            raise ValueError(
                f"additionalAffiliatedMerchantId cannot exceed {MAX_ADDITIONAL_AFFILIATED_MERCHANTS} "
                "entries (10 total with the primary affiliatedMerchantId, T-4)"
            )
        return v

    @model_validator(mode="after")
    def _validate_cross_fields(self) -> "ApplicationProcessing":
        # AC-1 / T-1 — hard gate, not just an advisory hint.
        total = self.cardPresent + self.eCommerce + self.manuallyKeyedMoto
        if total != 100:
            raise ValueError(
                "cardPresent + eCommerce + manuallyKeyedMoto must total exactly 100 "
                f"(got {total}) — GP 40014"
            )

        # AC-2 / T-3 — webSiteAddress required exactly when eCommerce > 0.
        if self.eCommerce > 0 and not self.webSiteAddress:
            raise ValueError("webSiteAddress is required when eCommerce > 0")
        if self.eCommerce == 0 and self.webSiteAddress:
            raise ValueError("webSiteAddress must be omitted when eCommerce = 0")

        # AC-3 / T-2 — billPriorToShip gates prepaymentType/paymentCollected.
        if self.billPriorToShip:
            if not self.prepaymentType:
                raise ValueError("prepaymentType is required when billPriorToShip=true")
            if not self.paymentCollected:
                raise ValueError("paymentCollected is required when billPriorToShip=true")
        else:
            if self.prepaymentType is not None:
                raise ValueError("prepaymentType must be omitted when billPriorToShip=false")
            if self.paymentCollected is not None:
                raise ValueError("paymentCollected must be omitted when billPriorToShip=false")

        # AC-4 / T-5 — no duplicate affiliated merchant IDs across primary + additional.
        if self.additionalAffiliatedMerchantId:
            ids = [e.affiliatedMerchantId for e in self.additionalAffiliatedMerchantId]
            if self.affiliatedMerchantId is not None:
                ids.append(self.affiliatedMerchantId)
            if len(ids) != len(set(ids)):
                raise ValueError(
                    "Duplicate affiliated merchant IDs are not allowed across "
                    "affiliatedMerchantId and additionalAffiliatedMerchantId (GP 40025)"
                )
        return self


# ---------------------------------------------------------------------------
# applicationPatriotAct
# ---------------------------------------------------------------------------


class ApplicationPatriotAct(BaseModel):
    governmentIdType: Optional[str] = None
    businessIdType: Optional[str] = None
    businessIdPlaceOfIssuance: Optional[str] = None
    businessIdDateIssued: Optional[str] = None
    businessIdDateExpired: Optional[str] = None
    businessIdNumber: Optional[str] = None
    govtThirdPartyDescription: Optional[str] = None

    @field_validator("governmentIdType")
    @classmethod
    def _validate_government_id_type(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and v not in GOVERNMENT_ID_TYPE_CHOICES:
            raise ValueError(f"governmentIdType must be one of {GOVERNMENT_ID_TYPE_CHOICES}")
        return v

    @field_validator("businessIdType")
    @classmethod
    def _validate_business_id_type(cls, v: Optional[str]) -> Optional[str]:
        if v is not None and v not in BUSINESS_ID_TYPE_CHOICES:
            raise ValueError(f"businessIdType must be one of {BUSINESS_ID_TYPE_CHOICES}")
        return v

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

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

    @model_validator(mode="after")
    def _validate_conditionals(self) -> "ApplicationPatriotAct":
        # governmentIdType == TPV -> govtThirdPartyDescription required.
        if self.governmentIdType == "TPV" and not self.govtThirdPartyDescription:
            raise ValueError("govtThirdPartyDescription is required when governmentIdType=TPV")
        if self.governmentIdType != "TPV" and self.govtThirdPartyDescription:
            raise ValueError("govtThirdPartyDescription must be omitted unless governmentIdType=TPV")

        bid_type = self.businessIdType
        if bid_type in BUSINESS_ID_TYPE_REQUIRES_PLACE_OF_ISSUANCE and not self.businessIdPlaceOfIssuance:
            raise ValueError(
                f"businessIdPlaceOfIssuance is required when businessIdType={bid_type!r}"
            )
        if bid_type in BUSINESS_ID_TYPE_REQUIRES_DATE_ISSUED and not self.businessIdDateIssued:
            raise ValueError(
                f"businessIdDateIssued is required when businessIdType={bid_type!r}"
            )
        if bid_type == "GIBL" and not self.businessIdDateExpired:
            raise ValueError("businessIdDateExpired is required when businessIdType=GIBL")
        if bid_type == "TR" and not self.businessIdNumber:
            raise ValueError("businessIdNumber is required when businessIdType=TR")

        if self.businessIdDateIssued:
            issued = _parse_date_field(self.businessIdDateIssued, "businessIdDateIssued")
            if issued >= date.today():
                raise ValueError("businessIdDateIssued must be a past date")
        if self.businessIdDateExpired:
            expired = _parse_date_field(self.businessIdDateExpired, "businessIdDateExpired")
            if expired <= date.today():
                raise ValueError("businessIdDateExpired must be a future date")
        return self


# ---------------------------------------------------------------------------
# applicationMerchantSurvey
# ---------------------------------------------------------------------------


class ApplicationMerchantSurvey(BaseModel):
    """C-07 (CERT-confirmed): must be the nested-object shape below — a bare
    enum string is REJECTED (`applicationMerchantSurvey value is invalid`)."""

    onSiteVisit: bool
    inventoryMaintained: Optional[bool] = None
    inventorySufficient: Optional[bool] = None
    merchandiseMatch: Optional[bool] = None
    signageCompliant: Optional[bool] = None
    notes: Optional[str] = None

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

    @model_validator(mode="after")
    def _validate_onsite_conditionals(self) -> "ApplicationMerchantSurvey":
        if self.onSiteVisit:
            missing = [
                name
                for name in (
                    "inventoryMaintained",
                    "inventorySufficient",
                    "merchandiseMatch",
                    "signageCompliant",
                    "notes",
                )
                if getattr(self, name) is None
            ]
            if missing:
                raise ValueError(
                    f"{', '.join(missing)} required when onSiteVisit=true"
                )
        else:
            populated = [
                name
                for name in (
                    "inventoryMaintained",
                    "inventorySufficient",
                    "merchandiseMatch",
                    "signageCompliant",
                    "notes",
                )
                if getattr(self, name) is not None
            ]
            if populated:
                raise ValueError(
                    f"{', '.join(populated)} must be omitted when onSiteVisit=false"
                )
        return self


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


class ProcessingInformationRequest(BaseModel):
    """`PUT /onboarding/processing` request body — PRD-HWONB-004 §1/§3."""

    model_config = ConfigDict(str_strip_whitespace=True)

    applicationProcessing: ApplicationProcessing
    applicationPatriotAct: ApplicationPatriotAct
    applicationMerchantSurvey: ApplicationMerchantSurvey

    def to_gp_payload(self) -> dict:
        return self.model_dump(exclude_none=True)


class ProcessingInformationSaveResponse(BaseModel):
    """PRD-HWONB-004 §3 — same envelope shape as Business's save response."""

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