from datetime import datetime
from typing import Optional
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, field_validator

# ── Shared field constraints (single source of truth for the Add/Edit form and
# the CSV importer, so bulk import can never bypass single-plot rules — A03/A07) ──
_LAT_MIN, _LAT_MAX = -90.0, 90.0
_LNG_MIN, _LNG_MAX = -180.0, 180.0
_PRICE_MAX = 9_999_999.99


def _validate_lat(v: Optional[float]) -> Optional[float]:
    if v is not None and not (_LAT_MIN <= v <= _LAT_MAX):
        raise ValueError("Latitude must be between -90 and 90")
    return v


def _validate_lng(v: Optional[float]) -> Optional[float]:
    if v is not None and not (_LNG_MIN <= v <= _LNG_MAX):
        raise ValueError("Longitude must be between -180 and 180")
    return v


def _validate_price(v: Optional[float]) -> Optional[float]:
    if v is not None and (v < 0 or v > _PRICE_MAX):
        raise ValueError("Price must be between 0 and 9,999,999.99")
    return v


class CreatePlotRequest(BaseModel):
    # extra="forbid" blocks mass-assignment of protected columns (tenant_id, id,
    # deleted_at, geometry, …) smuggled in the request body — OWASP API3 / SEC-03.
    model_config = ConfigDict(extra="forbid")

    plot_ref: str = Field(min_length=1, max_length=50)
    section_id: Optional[UUID] = None
    # INDL-55 E-15: Plot Type is mandatory on create — it is the source of the
    # plot's dimensions (Length/Width/Depth live on the plot_type master, INDL-51),
    # so a plot with no type has no dimensions. Enforced server-side (AC-28).
    plot_type_id: UUID = Field(..., description="Plot type is required")
    # INDL-55 E-13: Availability is expressed through the tenant's plot_statuses
    # catalogue; a plot always resolves to a defined availability state. Defaults to
    # 'vacant' (available) when the caller omits it, so availability is never null.
    status: str = "vacant"
    latitude: Optional[float] = None
    longitude: Optional[float] = None
    # INDL-55 E-13: Price is mandatory on create (AC-24). Range enforced by _validate_price.
    price_override: float = Field(..., ge=0, le=_PRICE_MAX, description="Price is required")
    notes: Optional[str] = Field(default=None, max_length=2000)
    public_description: Optional[str] = Field(default=None, max_length=180)
    label_manual: Optional[str] = Field(default=None, max_length=50)
    is_veteran_section: bool = False

    _v_lat = field_validator("latitude")(_validate_lat)
    _v_lng = field_validator("longitude")(_validate_lng)


class UpdatePlotRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    plot_ref: Optional[str] = Field(default=None, min_length=1, max_length=50)
    section_id: Optional[UUID] = None
    plot_type_id: Optional[UUID] = None
    status: Optional[str] = None
    latitude: Optional[float] = None
    longitude: Optional[float] = None
    price_override: Optional[float] = None
    notes: Optional[str] = Field(default=None, max_length=2000)
    public_description: Optional[str] = Field(default=None, max_length=180)
    is_veteran_section: Optional[bool] = None

    # INDL-41 — geometry & detail-panel fields (editor mode / admin-manager only).
    # `geometry` is a GeoJSON Polygon; the service recomputes centroid + lat/lng.
    geometry: Optional[dict] = None
    rotation_deg: Optional[float] = None
    reserved_by: Optional[str] = Field(None, max_length=255)
    reserved_at: Optional[datetime] = None
    label_manual: Optional[str] = Field(None, max_length=50)

    _v_lat = field_validator("latitude")(_validate_lat)
    _v_lng = field_validator("longitude")(_validate_lng)
    _v_price = field_validator("price_override")(_validate_price)


class ChangePlotStatusRequest(BaseModel):
    status: str  # vacant / reserved / occupied / hold


# ── CSV / Excel bulk import (INDL-42a / INDL-42g) ────────────────────────────


class ImportColumnMapping(BaseModel):
    """One source-column → INDELIS-field mapping produced by Step 2 of the wizard."""

    model_config = ConfigDict(extra="forbid")

    source: str = Field(min_length=1, max_length=200)
    target: Optional[str] = None  # None / "" / "ignore" = drop the column


class SuggestMappingRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    columns: list[str] = Field(min_length=1, max_length=200)
