from datetime import datetime
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List, Dict

class TemporalConfigBase(BaseModel):
    version: int = Field(default=1, ge=1)
    is_active: bool = Field(default=True)
    effective_from: datetime = Field(default_factory=datetime.utcnow)
    effective_to: Optional[datetime] = None
    created_by: Optional[str] = None
    updated_by: Optional[str] = None

class LenderCreate(BaseModel):
    name: str = Field(..., min_length=2, max_length=255)
    code: str = Field(..., min_length=2, max_length=50)

class LenderOut(LenderCreate):
    id: int
    is_active: bool
    class Config:
        from_attributes = True

class ProductCreate(BaseModel):
    lender_id: int
    name: str = Field(..., min_length=2, max_length=255)
    code: str = Field(..., min_length=2, max_length=100)

class ProductOut(ProductCreate):
    id: int
    is_active: bool
    class Config:
        from_attributes = True

class LenderUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=2, max_length=255)
    code: Optional[str] = Field(None, min_length=2, max_length=50)
    is_active: Optional[bool] = None

class ProductUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=2, max_length=255)
    code: Optional[str] = Field(None, min_length=2, max_length=100)
    is_active: Optional[bool] = None

class InterestConfigCreate(TemporalConfigBase):
    product_id: int
    calculation_basis: str  # ACTUAL_365, ACTUAL_360, 30_360, ACTUAL_ACTUAL, DAYS360, IPMT
    emi_type: str  # FLAT, REDUCING
    repayment_frequency: str = "MONTHLY"  # MONTHLY, WEEKLY, DAILY
    grace_period_days: int = Field(default=0, ge=0)

    # BPI & Rounding Configuration
    bpi_applicable: bool = True  # Whether BPI is applicable for this product
    bpi_treatment: str = "ADD_TO_LOAN"  # ADD_TO_LOAN, SEPARATE_BEFORE_EMI, ADD_IN_EMI
    bpi_day_convention: str = "ACTUAL_365"  # 30_360, ACTUAL_ACTUAL, ACTUAL_365, DAYS360, IPMT
    emi_rounding: str = "ROUND"  # ROUND_UP, ROUND_DOWN, ROUND
    emi_rounding_digits: int = Field(default=0)  # 0, 1, 2, -1  (no -2 for EMI)
    interest_rounding: str = "ROUND"  # ROUND_UP, ROUND_DOWN, ROUND
    interest_rounding_digits: int = Field(default=0)  # 0, 1, 2, -1, -2
    bpi_rounding: str = "ROUND"  # ROUND_UP, ROUND_DOWN, ROUND
    bpi_rounding_digits: int = Field(default=0)  # 0, 1, 2, -1, -2

    @field_validator("calculation_basis")
    @classmethod
    def validate_basis(cls, v: str) -> str:
        allowed = ["ACTUAL_365", "ACTUAL_360", "30_360", "ACTUAL_ACTUAL", "DAYS360", "IPMT"]
        if v not in allowed:
            raise ValueError(f"calculation_basis must be one of {allowed}")
        return v

    @field_validator("emi_type")
    @classmethod
    def validate_emi_type(cls, v: str) -> str:
        if v not in ["FLAT", "REDUCING"]:
            raise ValueError("emi_type must be FLAT or REDUCING")
        return v

    @field_validator("bpi_treatment")
    @classmethod
    def validate_bpi_treatment(cls, v: str) -> str:
        allowed = ["ADD_TO_LOAN", "SEPARATE_BEFORE_EMI", "ADD_IN_EMI"]
        if v not in allowed:
            raise ValueError(f"bpi_treatment must be one of {allowed}")
        return v

    @field_validator("bpi_day_convention")
    @classmethod
    def validate_bpi_day_convention(cls, v: str) -> str:
        allowed = ["30_360", "ACTUAL_ACTUAL", "ACTUAL_365", "DAYS360", "IPMT"]
        if v not in allowed:
            raise ValueError(f"bpi_day_convention must be one of {allowed}")
        return v

    @field_validator("emi_rounding", "interest_rounding", "bpi_rounding")
    @classmethod
    def validate_rounding(cls, v: str) -> str:
        if v not in ["ROUND_UP", "ROUND_DOWN", "ROUND"]:
            raise ValueError("rounding must be ROUND_UP, ROUND_DOWN, or ROUND")
        return v

    @field_validator("emi_rounding_digits")
    @classmethod
    def validate_emi_rounding_digits(cls, v: int) -> int:
        if v not in [0, 1, 2, -1]:
            raise ValueError("emi_rounding_digits must be 0, 1, 2, or -1")
        return v

    @field_validator("interest_rounding_digits", "bpi_rounding_digits")
    @classmethod
    def validate_rounding_digits(cls, v: int) -> int:
        if v not in [0, 1, 2, -1, -2]:
            raise ValueError("rounding_digits must be 0, 1, 2, -1, or -2")
        return v

class AllocationConfigCreate(TemporalConfigBase):
    product_id: int
    sequence_type: str  # REGULAR_EMI, SETTLEMENT, FORECLOSURE, NPA, ADVANCE_COLLECTION
    priority_sequence: List[str]  # e.g., ["interest", "principal", "charges"]

    @field_validator("sequence_type")
    @classmethod
    def validate_seq_type(cls, v: str) -> str:
        if v not in ["REGULAR_EMI", "SETTLEMENT", "FORECLOSURE", "NPA", "ADVANCE_COLLECTION"]:
            raise ValueError("sequence_type must be REGULAR_EMI, ADVANCE_COLLECTION, SETTLEMENT, FORECLOSURE, or NPA")
        return v

    @field_validator("priority_sequence")
    @classmethod
    def validate_priority(cls, v: List[str]) -> List[str]:
        valid_items = ["interest", "principal", "charges"]
        for item in v:
            if item not in valid_items:
                raise ValueError(f"Priority sequence items must be one of {valid_items}")
        return v

class SpecificCollectionConfigCreate(TemporalConfigBase):
    product_id: int
    collection_type: str  # e.g., ADVANCE_COLLECTION
    park_collection: bool = Field(default=False)
    metadata_config: dict | None = None

class ChargeConfigCreate(TemporalConfigBase):
    product_id: int
    charge_master_id: int
    charge_type: str  # FIXED, PERCENTAGE
    value: float = Field(..., ge=0.0)
    is_taxable: bool = Field(default=True)
    tax_percentage: float = Field(default=18.00, ge=0.0)
    partner_split: Optional[Dict[str, float]] = None

    @field_validator("charge_type")
    @classmethod
    def validate_charge_type(cls, v: str) -> str:
        if v not in ["FIXED", "PERCENTAGE", "NONE"]:
            raise ValueError("charge_type must be FIXED, PERCENTAGE, or NONE")
        return v

class ForeclosureConfigCreate(TemporalConfigBase):
    product_id: int
    charge_type: str  # FIXED, PERCENTAGE, NONE
    charge_value: float = Field(..., ge=0.0)
    lock_in_period_months: int = Field(default=0, ge=0)
    rebate_percentage: float = Field(default=0.00, ge=0.0, le=100.0)

    @field_validator("charge_type")
    @classmethod
    def validate_charge_type(cls, v: str) -> str:
        if v not in ["FIXED", "PERCENTAGE", "NONE"]:
            raise ValueError("charge_type must be FIXED, PERCENTAGE, or NONE")
        return v

class NpaConfigCreate(TemporalConfigBase):
    product_id: int
    dpd_threshold: int = Field(default=90, ge=0)
    bucket_classification_rules: Dict[str, str]  # e.g., {"0-30": "Standard", "31-60": "SMA-1", "90+": "NPA"}

class DisbursementConfigCreate(TemporalConfigBase):
    product_id: int
    disbursement_logic: str  # ON_OR_BEFORE_DATE, FIXED_DATE
    fixed_day_of_month: Optional[int] = Field(default=None, ge=1, le=31)
    cut_off_hour: int = Field(default=18, ge=0, le=23)

    @field_validator("disbursement_logic")
    @classmethod
    def validate_disb_logic(cls, v: str) -> str:
        if v not in ["ON_OR_BEFORE_DATE", "FIXED_DATE"]:
            raise ValueError("disbursement_logic must be ON_OR_BEFORE_DATE or FIXED_DATE")
        return v

class EmiConfigCreate(TemporalConfigBase):
    product_id: int
    emi_due_day: int = Field(default=3, ge=1, le=31)


class CollectionConfigCreate(TemporalConfigBase):
    product_id: int
    allocation_strategy: str  # OLDEST_DUE_FIRST, DUE_DATE_BASED, FUTURE_ALLOCATION
    advance_collection_strategy: str  # NO_INTEREST_BENEFIT, INTEREST_BENEFIT
    excess_refund_required: bool = True

ALLOWED_WAIVE_CATEGORIES = {"BOUNCE", "PENAL"}

class WaiveOffConfigCreate(TemporalConfigBase):
    product_id: int
    # Ordered list of charge categories eligible for waiver. Index 0 = highest priority (waived first).
    # Only "BOUNCE" and "PENAL" are permitted values.
    waiveable_charge_categories: List[str]

    @field_validator("waiveable_charge_categories")
    @classmethod
    def validate_categories(cls, v: List[str]) -> List[str]:
        if not v:
            raise ValueError("waiveable_charge_categories must contain at least one category")
        if len(v) > 2:
            raise ValueError("At most 2 categories are allowed: BOUNCE and PENAL")
        seen = set()
        for item in v:
            if item not in ALLOWED_WAIVE_CATEGORIES:
                raise ValueError(f"'{item}' is not a valid waiveable category. Allowed: {sorted(ALLOWED_WAIVE_CATEGORIES)}")
            if item in seen:
                raise ValueError(f"Duplicate category '{item}' found in waiveable_charge_categories")
            seen.add(item)
        return v

ALLOWED_RPS_PARTIES = {"primary_lender", "co-lender"}

class RpsSplitPart(BaseModel):
    """
    A single tranche in an RPS split option.
    pct          — percentage of POS (1–100, all parts in a split must sum to 100)
    party        — who this tranche belongs to: 'primary_lender' or 'co-lender'
    hurdle_rate  — optional minimum return % guaranteed to primary_lender on this tranche;
                   only applicable when party == 'primary_lender', ignored for co-lender.
    """
    pct: int = Field(..., ge=1, le=100)
    party: str
    hurdle_rate: Optional[float] = Field(default=None, ge=0.0, le=100.0)

    @field_validator("party")
    @classmethod
    def validate_party(cls, v: str) -> str:
        if v not in ALLOWED_RPS_PARTIES:
            raise ValueError(f"party must be one of {sorted(ALLOWED_RPS_PARTIES)}")
        return v

class RpsSplitOption(BaseModel):
    """
    One complete split option (all parts must sum to 100).
    E.g. parts=[{pct:80,party:'primary_lender',hurdle_rate:12.5},{pct:20,party:'co-lender'}]
    """
    parts: List[RpsSplitPart]

class RpsConfigCreate(TemporalConfigBase):
    product_id: int
    # List of allowed POS split options.
    # Each option is an object with a 'parts' list whose pct values must sum to 100.
    # E.g., [{"parts":[{"pct":100,"party":"primary_lender","hurdle_rate":12.0}]},
    #         {"parts":[{"pct":80,"party":"primary_lender","hurdle_rate":10.0},
    #                   {"pct":20,"party":"co-lender"}]}]
    allowed_splits: List[RpsSplitOption]

    @field_validator("allowed_splits", mode="before")
    @classmethod
    def migrate_and_validate_splits(cls, v):
        """Accept legacy List[List[int]], old 'lender' party name, and new structured formats."""
        if not v:
            raise ValueError("allowed_splits must contain at least one split option")
        if len(v) > 10:
            raise ValueError("A maximum of 10 split options are allowed per product")

        normalised = []
        for i, split in enumerate(v):
            # Legacy format: plain list of ints → auto-assign parties
            if isinstance(split, list):
                parts = []
                for j, pct in enumerate(split):
                    party = "primary_lender" if j == 0 else "co-lender"
                    parts.append({"pct": pct, "party": party})
                normalised.append({"parts": parts})
            elif isinstance(split, dict):
                # Migrate old 'lender' party name → 'primary_lender'
                parts = split.get("parts", [])
                migrated_parts = []
                for p in parts:
                    part = dict(p) if isinstance(p, dict) else p
                    if isinstance(part, dict) and part.get("party") == "lender":
                        part["party"] = "primary_lender"
                    migrated_parts.append(part)
                normalised.append({"parts": migrated_parts})
            else:
                normalised.append(split)

        # Validate sums
        for i, opt in enumerate(normalised):
            parts = opt["parts"] if isinstance(opt, dict) else opt.parts
            total = sum((p["pct"] if isinstance(p, dict) else p.pct) for p in parts)
            if total != 100:
                raise ValueError(
                    f"Split option at index {i} does not sum to 100 (got {total})"
                )

        # Validate hurdle_rate uniqueness — at most ONE split option may carry a hurdle_rate
        splits_with_hurdle = []
        for i, opt in enumerate(normalised):
            parts = opt["parts"] if isinstance(opt, dict) else opt.parts
            for p in parts:
                hr = p.get("hurdle_rate") if isinstance(p, dict) else p.hurdle_rate
                party = p.get("party") if isinstance(p, dict) else p.party
                if hr is not None and party == "primary_lender":
                    splits_with_hurdle.append(i)
                    break
        if len(splits_with_hurdle) > 1:
            raise ValueError(
                f"Hurdle Rate can only be set on one split option at a time. "
                f"Found hurdle_rate on split indices: {splits_with_hurdle}. "
                f"Please set it on a single split only."
            )
        return normalised
