"""Pydantic models for the NPL (New Product Listing) module.

Read-side: NplProductRead carries the master + the fan-out collections
(stock_allocation, forecast, substitution rows). Write-side: Create /
Update variants accept partial payloads and reject inputs that violate
DB-side CHECK constraints early so the user gets a clean 400 instead of
an opaque psycopg integrity error.
"""
from __future__ import annotations

from datetime import date, datetime
from typing import Optional

from pydantic import BaseModel, Field, model_validator


# ----------------------------------------------------------------------
# Enum value lists (kept in sync with db/npl_schema.sql)
# ----------------------------------------------------------------------
NPL_GROUPS              = ("food", "supplement", "accessories", "other")
NPL_RELATIONSHIP_TYPES  = ("soft", "hard")
NPL_STATUSES = (
    "draft", "approved", "fmb_triggered", "ordered",
    "in_stock", "active", "phase_out",
)
NPL_CHANNELS  = ("mp", "vp", "exp")
NPL_FC_TYPES  = ("regular", "sample")


# ======================================================================
# Stock allocation
# ======================================================================
class NplStockAllocationBase(BaseModel):
    channel: str
    customer_id: Optional[int] = None
    sample_qty: Optional[int] = None
    safety_qty: int = 0
    stocking_qty: int = 0

    @model_validator(mode="after")
    def _check(self) -> "NplStockAllocationBase":
        if self.channel not in NPL_CHANNELS:
            raise ValueError(f"channel must be one of {NPL_CHANNELS}")
        if self.channel == "mp" and self.customer_id is not None:
            raise ValueError("MP allocation must have customer_id NULL")
        if self.channel == "vp" and self.customer_id is None:
            raise ValueError("VP allocation requires customer_id")
        if self.channel != "vp" and self.sample_qty is not None:
            raise ValueError("sample_qty only allowed on VP channel")
        return self


class NplStockAllocationCreate(NplStockAllocationBase):
    pass


class NplStockAllocationRead(NplStockAllocationBase):
    id: int
    npl_product_id: int
    created_at: datetime
    updated_at: datetime


# ======================================================================
# Forecast
# ======================================================================
class NplForecastBase(BaseModel):
    channel: str                # 'mp' | 'vp' (Exp is per-deal, not persisted)
    customer_id: Optional[int] = None
    fc_type: str = "regular"    # 'regular' | 'sample'
    year: int
    week: int = Field(..., ge=1, le=53)
    qty: int = 0

    @model_validator(mode="after")
    def _check(self) -> "NplForecastBase":
        if self.channel not in ("mp", "vp"):
            raise ValueError("forecast.channel must be 'mp' or 'vp'")
        if self.fc_type not in NPL_FC_TYPES:
            raise ValueError(f"fc_type must be one of {NPL_FC_TYPES}")
        if self.channel == "mp" and (self.customer_id is not None
                                     or self.fc_type != "regular"):
            raise ValueError("MP forecast: customer_id NULL & fc_type='regular'")
        if self.channel == "vp" and self.customer_id is None:
            raise ValueError("VP forecast requires customer_id")
        return self


class NplForecastCreate(NplForecastBase):
    pass


class NplForecastRead(NplForecastBase):
    id: int
    npl_product_id: int
    created_at: datetime
    updated_at: datetime


# ======================================================================
# Substitution
# ======================================================================
class NplSubstitutionBase(BaseModel):
    parent_product_id: int
    channel: str
    is_substitutable: bool

    @model_validator(mode="after")
    def _check(self) -> "NplSubstitutionBase":
        if self.channel not in NPL_CHANNELS:
            raise ValueError(f"channel must be one of {NPL_CHANNELS}")
        return self


class NplSubstitutionCreate(NplSubstitutionBase):
    pass


class NplSubstitutionRead(NplSubstitutionBase):
    id: int
    npl_product_id: int
    created_at: datetime


# ======================================================================
# NPL Product (master)
# ======================================================================
class NplProductBase(BaseModel):
    sku: str = Field(..., min_length=1)
    name: str = Field(..., min_length=1)
    group_enum: str
    channels: list[str]                   # JSON array, must contain 'mp'
    launch_date: date
    barcode: str
    parent_sku_id: Optional[int] = None
    relationship_type: Optional[str] = None
    supplier_id: int
    cost_price: float
    volume_pricing: Optional[dict] = None
    lead_time_weeks: int = Field(..., ge=0)
    safety_buffer_weeks: int = Field(..., ge=0)
    npl_flag: bool = True

    @model_validator(mode="after")
    def _check(self) -> "NplProductBase":
        if self.group_enum not in NPL_GROUPS:
            raise ValueError(f"group must be one of {NPL_GROUPS}")
        if not isinstance(self.channels, list) or not self.channels:
            raise ValueError("channels must be a non-empty list")
        for c in self.channels:
            if c not in NPL_CHANNELS:
                raise ValueError(f"channel '{c}' invalid; allowed {NPL_CHANNELS}")
        if "mp" not in self.channels:
            raise ValueError("channels must always include 'mp'")
        if (self.parent_sku_id is None) != (self.relationship_type is None):
            raise ValueError(
                "parent_sku_id and relationship_type must be set together "
                "(or both omitted)"
            )
        if self.relationship_type is not None \
                and self.relationship_type not in NPL_RELATIONSHIP_TYPES:
            raise ValueError(
                f"relationship_type must be one of {NPL_RELATIONSHIP_TYPES}"
            )
        return self


class NplProductCreate(NplProductBase):
    """POST /api/npl — body for create.  Optional nested arrays let a
    caller seed allocation / forecast rows in the same request."""
    stock_allocation: list[NplStockAllocationCreate] = []
    forecast: list[NplForecastCreate] = []
    substitution: list[NplSubstitutionCreate] = []


class NplProductUpdate(BaseModel):
    """PUT /api/npl/{id} — every field optional; service merges into the
    existing row.  Constraint checks run again on the merged result."""
    name: Optional[str] = None
    group_enum: Optional[str] = None
    channels: Optional[list[str]] = None
    launch_date: Optional[date] = None
    barcode: Optional[str] = None
    parent_sku_id: Optional[int] = None
    relationship_type: Optional[str] = None
    supplier_id: Optional[int] = None
    cost_price: Optional[float] = None
    volume_pricing: Optional[dict] = None
    lead_time_weeks: Optional[int] = Field(None, ge=0)
    safety_buffer_weeks: Optional[int] = Field(None, ge=0)
    npl_flag: Optional[bool] = None
    status: Optional[str] = None         # admin status override


class NplProductRead(NplProductBase):
    id: int
    fmb_date: Optional[date] = None      # generated column
    status: str
    approved_by: Optional[int] = None
    approved_at: Optional[datetime] = None
    created_by: int
    created_at: datetime
    updated_at: datetime
    # Joined display fields (resolved by service)
    supplier_name: Optional[str] = None
    parent_sku_code: Optional[str] = None
    # Nested rows
    stock_allocation: list[NplStockAllocationRead] = []
    forecast: list[NplForecastRead] = []
    substitution: list[NplSubstitutionRead] = []


# ======================================================================
# List view (compact summary, no nested rows)
# ======================================================================
class NplProductListRow(BaseModel):
    id: int
    sku: str
    name: str
    group_enum: str
    channels: list[str]
    status: str
    launch_date: date
    fmb_date: Optional[date] = None
    supplier_id: int
    supplier_name: Optional[str] = None
    parent_sku_id: Optional[int] = None
    parent_sku_code: Optional[str] = None
    relationship_type: Optional[str] = None
    lead_time_weeks: int
    safety_buffer_weeks: int
    cost_price: float
    npl_flag: bool
    created_at: datetime
    updated_at: datetime


class NplListResponse(BaseModel):
    rows: list[NplProductListRow]
    total: int
    by_status: dict[str, int] = {}
    by_group:  dict[str, int] = {}


# ======================================================================
# Dashboard summary (top-of-page widget)
# ======================================================================
class NplDashboardSummary(BaseModel):
    total_npl: int
    by_status: dict[str, int]
    by_group: dict[str, int]
    fmb_due_within_14d: int             # FMB date in [today, today+14]
    fmb_overdue: int                    # FMB date in past, not yet triggered
    launching_within_30d: int           # launch_date in [today, today+30]
    note: Optional[str] = None


# ======================================================================
# Lifecycle endpoints — bodies + responses
# ======================================================================
class NplApprovePayload(BaseModel):
    """POST /api/npl/{id}/approve"""
    note: Optional[str] = None


class NplStatusChangeResponse(BaseModel):
    id: int
    sku: str
    status: str
    message: str


class NplFmbCheckResponse(BaseModel):
    """Daily FMB sweep — returns the SKUs that just transitioned to
    'fmb_triggered' so the caller can notify Nabava."""
    triggered: list[NplStatusChangeResponse]
    checked: int
    today: date


# ======================================================================
# Enum admin (Admin only)
# ======================================================================
class NplEnumValues(BaseModel):
    groups: list[str]
    relationship_types: list[str]
    statuses: list[str]
    channels: list[str]
    fc_types: list[str]


# ======================================================================
# Helpers — supplier picker
# ======================================================================
class NplSupplierOption(BaseModel):
    """Shape for the supplier autocomplete dropdown in the detail form."""
    id: int
    name: str


# ======================================================================
# NPL Report — actuals for NPD (newly-arrived) products
# ======================================================================
class NplReportRow(BaseModel):
    """One row per NPD SKU with sales-to-date roll-up."""
    sku: str
    name: str
    brand: Optional[str] = None
    manufacturer: Optional[str] = None
    launch_month: Optional[str] = None    # 'YYYY-MM' or None
    arrived: bool
    cost_price: Optional[float] = None
    avg_sell_price: Optional[float] = None
    units_total: float = 0
    revenue_total: float = 0              # qty × avg_sell_price
    ruc_total: float = 0                  # margin in EUR (from erp_transactions)
    n_weeks_with_sales: int = 0
    last_sale_year_week: Optional[int] = None


class NplReportListResponse(BaseModel):
    rows: list[NplReportRow]
    total_skus: int
    total_units: float
    total_revenue: float
    total_ruc: float


class NplReportPoint(BaseModel):
    period: str                            # 'CWxx 2026' (weekly) or '2026-04' (monthly)
    year: int
    bucket: int                            # week or month number
    units: float = 0
    revenue: float = 0
    ruc: float = 0


class NplReportTimeseriesResponse(BaseModel):
    sku: str
    name: Optional[str] = None
    granularity: str                       # 'week' | 'month'
    points: list[NplReportPoint]
    units_total: float
    revenue_total: float
    ruc_total: float
