from __future__ import annotations

from enum import Enum
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, Field, field_validator, model_validator

if TYPE_CHECKING:
    from collections.abc import Iterable


class PrimaryTankMode(str, Enum):
    ON = "on"
    OFF = "off"
    AUTO = "auto"


class Product(BaseModel):
    product_type: str
    name: str | None = None


class Config(BaseModel):
    directive_min_percent: float = Field(default=0.85, ge=0, le=1)
    directive_max_percent: float = Field(default=1.10, ge=1)
    under_directive_penalty: float = Field(default=5.0, ge=0)
    max_model_run_time: int = Field(default=60, ge=1)
    fake_supply_cost: float = Field(default=10.0, ge=0)
    truck_size: float = Field(default=8000.0, gt=0)
    enable_secondary_tank_following: bool = Field(default=False)
    primary_tank_mode: PrimaryTankMode = Field(default=PrimaryTankMode.AUTO)
    solver: str | None = Field(default=None, description="Solver to use: auto, PULP_CBC_CMD, GUROBI_CMD, CPLEX_CMD")
    auto_calculate_primary_tanks: bool | None = Field(default=None, exclude=True)

    @field_validator("primary_tank_mode", mode="before")
    @classmethod
    def convert_legacy_auto_calculate_primary_tanks(cls, v, info):
        # Handle backward compatibility for old boolean field
        if isinstance(v, bool):
            return PrimaryTankMode.ON if v else PrimaryTankMode.OFF
        return v

    @model_validator(mode="before")
    @classmethod
    def handle_legacy_auto_calculate_primary_tanks(cls, data: Any) -> Any:
        # If auto_calculate_primary_tanks is provided but primary_tank_mode is not
        if (
            isinstance(data, dict)
            and "auto_calculate_primary_tanks" in data
            and "primary_tank_mode" not in data
        ):
            auto_calc = data["auto_calculate_primary_tanks"]
            data["primary_tank_mode"] = PrimaryTankMode.ON if auto_calc else PrimaryTankMode.OFF
        return data

    @field_validator("directive_max_percent")
    @classmethod
    def max_greater_than_min(cls, v, info):
        if (
            "directive_min_percent" in info.data
            and v <= info.data["directive_min_percent"]
        ):
            raise ValueError(
                "directive_max_percent must be greater than directive_min_percent"
            )
        return v


class Tank(BaseModel):
    tank_id: str
    product_type: str
    demand: float = Field(ge=0)
    is_primary: bool = False
    product: Product | None = None


class Site(BaseModel):
    site_id: str
    name: str
    location: str
    tanks: list[Tank]

    @field_validator("tanks")
    @classmethod
    def tanks_not_empty(cls, v):
        if not v:
            raise ValueError("Site must have at least one tank")
        return v

    def _reset_all_tanks_to_secondary(self):
        for tank in self.tanks:
            tank.is_primary = False

    def set_primary_tanks(self, truck_size: float):
        self._reset_all_tanks_to_secondary()

        if high_demand_tanks := [
            tank for tank in self.tanks if tank.demand > truck_size
        ]:
            for tank in high_demand_tanks:
                tank.is_primary = True
        else:
            if self.tanks:
                highest_demand_tank = max(self.tanks, key=lambda t: t.demand)
                highest_demand_tank.is_primary = True


class Directive(BaseModel):
    contract_id: str
    supplier: str
    volume: float = Field(gt=0)
    supply_prices: list[DirectiveSupplyPrice]
    min_fill_percentage: float = Field(default=0.85, ge=0, le=1)
    max_fill_percentage: float = Field(default=1.10, ge=1)
    under_delivery_penalty: float = Field(default=0.05, ge=0)
    # Direct volume limits - when set, these override percentage-based calculations
    min_volume: float | None = Field(default=None, ge=0)
    max_volume: float | None = Field(default=None, ge=0)

    @field_validator("supply_prices")
    @classmethod
    def supply_prices_not_empty(cls, v):
        if not v:
            raise ValueError("Directive must have at least one supply price")
        return v

    @field_validator("max_fill_percentage")
    @classmethod
    def max_greater_than_min(cls, v, info):
        if "min_fill_percentage" in info.data and v <= info.data["min_fill_percentage"]:
            raise ValueError(
                "max_fill_percentage must be greater than min_fill_percentage"
            )
        return v

    @field_validator("max_volume")
    @classmethod
    def max_volume_greater_than_min(cls, v, info):
        if (
            v is not None
            and "min_volume" in info.data
            and info.data["min_volume"] is not None
            and v <= info.data["min_volume"]
        ):
            raise ValueError("max_volume must be greater than min_volume")
        return v

    @property
    def terminals(self) -> list[str]:
        return list(dict.fromkeys(sp.terminal for sp in self.supply_prices))

    @property
    def products(self) -> list[str]:
        return list(dict.fromkeys(sp.product_type for sp in self.supply_prices))

    def get_effective_min_volume(self) -> float:
        """Get the effective minimum volume - direct volume if set, otherwise percentage-based"""
        if self.min_volume is not None:
            return self.min_volume
        return self.volume * self.min_fill_percentage

    def get_effective_max_volume(self) -> float:
        """Get the effective maximum volume - direct volume if set, otherwise percentage-based"""
        if self.max_volume is not None:
            return self.max_volume
        return self.volume * self.max_fill_percentage

    def has_direct_volumes(self) -> bool:
        """Check if this directive uses direct volume limits instead of percentage-based"""
        return self.min_volume is not None or self.max_volume is not None

    def get_target_volume_display(self) -> str:
        """Get display string for target volume range"""
        if self.has_direct_volumes():
            min_vol = self.get_effective_min_volume()
            max_vol = self.get_effective_max_volume()
            if min_vol == max_vol:
                return f"{min_vol:,.0f}"
            else:
                return f"{min_vol:,.0f}-{max_vol:,.0f}"
        else:
            return f"{self.volume:,.0f}"

    def get_volume_status(self, assigned_volume: float) -> str:
        """Get volume status string for reporting"""
        if self.has_direct_volumes():
            min_vol = self.get_effective_min_volume()
            max_vol = self.get_effective_max_volume()

            if assigned_volume == 0:
                if min_vol == max_vol:
                    return f"Not Assigned (0/{min_vol:,.0f})"
                else:
                    return f"Not Assigned (0/{min_vol:,.0f}-{max_vol:,.0f})"
            elif assigned_volume < min_vol:
                return f"Below Target ({assigned_volume:,.0f}/{min_vol:,.0f})"
            elif assigned_volume > max_vol:
                return f"Above Target ({assigned_volume:,.0f}/{max_vol:,.0f})"
            else:
                if min_vol == max_vol:
                    return f"Within Range ({assigned_volume:,.0f}/{max_vol:,.0f})"
                else:
                    return f"Within Range ({assigned_volume:,.0f})"
        else:
            # Percentage-based - use min/max percentage bounds for proper status
            min_vol = self.get_effective_min_volume()
            max_vol = self.get_effective_max_volume()
            fulfillment_pct = (assigned_volume / self.volume * 100) if self.volume > 0 else 0

            if assigned_volume == 0:
                return f"Not Assigned ({fulfillment_pct:.1f}%)"
            elif assigned_volume < min_vol:
                return f"Below Target ({fulfillment_pct:.1f}%, min {self.min_fill_percentage * 100:.0f}%)"
            elif assigned_volume > max_vol:
                return f"Above Target ({fulfillment_pct:.1f}%, max {self.max_fill_percentage * 100:.0f}%)"
            else:
                return f"Within Range ({fulfillment_pct:.1f}%)"


class PointToPoint(BaseModel):
    origin_terminal: str
    destination_site: str
    product_type: str | None = None
    product_types: list[str] | None = Field(default=None, min_length=1)
    freight_rate: float = Field(ge=0)
    toll_rate: float = Field(ge=0)

    @model_validator(mode="after")
    def check_product_type_exclusivity(self):
        if self.product_type and self.product_types:
            raise ValueError("Provide either product_type or product_types, not both")
        if not self.product_type and not self.product_types:
            raise ValueError("Provide either product_type or product_types")
        return self

    @property
    def resolved_product_types(self) -> list[str]:
        if self.product_types:
            return self.product_types
        return [self.product_type]

    @property
    def total_transport_cost(self) -> float:
        return self.freight_rate + self.toll_rate


class DirectiveSupplyPrice(BaseModel):
    terminal: str
    product_type: str
    price_per_gallon: float = Field(ge=0)
    curve_id: str | None = None


class RackPrice(BaseModel):
    terminal: str
    product_type: str
    price_per_gallon: float = Field(ge=0)
    supplier: str = Field(default="Unknown")
    curve_id: str | None = None


class SupplyPrice(BaseModel):
    """Internal supply price used by optimizer. Not part of API input."""
    terminal: str
    product_type: str
    price_per_gallon: float = Field(ge=0)
    supplier: str = Field(default="Unknown")
    is_bulk: bool = True
    contract_id: str | None = None
    curve_id: str | None = None


class BlendComponent(BaseModel):
    product_type: str
    ratio: float = Field(gt=0, le=1)


class TerminalLink(BaseModel):
    """Defines a valid route between two terminals for multi-stop trips."""

    origin_terminal: str
    destination_terminal: str
    transit_cost_per_gallon: float = Field(ge=0)


class Blend(BaseModel):
    product_type: str  # final blended product, e.g. "regular"
    components: list[BlendComponent] = Field(min_length=1)

    @field_validator("components")
    @classmethod
    def ratios_must_sum_to_one(cls, v):
        total = sum(c.ratio for c in v)
        if abs(total - 1.0) > 1e-6:
            raise ValueError(f"Blend component ratios must sum to 1.0, got {total}")
        return v


class OptimizationInput(BaseModel):
    sites: list[Site]
    directives: list[Directive]
    point_to_point: list[PointToPoint]
    rack_prices: list[RackPrice] = Field(default_factory=list)
    blends: list[Blend] = Field(default_factory=list)
    terminal_links: list[TerminalLink] = Field(default_factory=list)
    config: Config = Field(default_factory=Config)

    @field_validator("rack_prices")
    @classmethod
    def rack_prices_unique(cls, v):
        seen = set()
        for rp in v:
            key = (rp.terminal, rp.product_type)
            if key in seen:
                raise ValueError(
                    f"Duplicate rack price for terminal={rp.terminal}, product_type={rp.product_type}"
                )
            seen.add(key)
        return v

    @property
    def all_supply_prices(self) -> list[SupplyPrice]:
        """Build unified internal SupplyPrice list from directives and rack prices."""
        prices: list[SupplyPrice] = []
        for directive in self.directives:
            for sp in directive.supply_prices:
                prices.append(
                    SupplyPrice(
                        terminal=sp.terminal,
                        product_type=sp.product_type,
                        price_per_gallon=sp.price_per_gallon,
                        supplier=directive.supplier,
                        contract_id=directive.contract_id,
                        curve_id=sp.curve_id,
                    )
                )
        for rp in self.rack_prices:
            prices.append(
                SupplyPrice(
                    terminal=rp.terminal,
                    product_type=rp.product_type,
                    price_per_gallon=rp.price_per_gallon,
                    supplier=rp.supplier,
                    contract_id=None,
                    curve_id=rp.curve_id,
                )
            )
        return prices

    def has_any_primary_tanks(self) -> bool:
        """Check if any tanks are already marked as primary"""
        for site in self.sites:
            for tank in site.tanks:
                if tank.is_primary:
                    return True
        return False

    def _handle_manifold_primary_logic(self):
        """If any tank in a merged group (manifold) is primary, mark the whole group as primary"""
        for site in self.sites:
            # Group tanks by their base tank group (for merged tanks with '+' in tank_id)
            tank_groups = {}
            for tank in site.tanks:
                if '+' in tank.tank_id:
                    # This is a merged tank, check if any part of the group is primary
                    base_ids = tank.tank_id.split('+')
                    for base_id in base_ids:
                        if base_id not in tank_groups:
                            tank_groups[base_id] = []
                        tank_groups[base_id].append(tank)

            # If any tank in a merged group is primary, mark all as primary
            for group_tanks in tank_groups.values():
                if any(tank.is_primary for tank in group_tanks):
                    for tank in group_tanks:
                        tank.is_primary = True

    def detect_and_set_primary_tanks(self):
        """Detect and set primary tanks based on the configured mode"""
        if self.config.primary_tank_mode == PrimaryTankMode.ON:
            # Always calculate primary tanks (overwrite existing)
            for site in self.sites:
                site.set_primary_tanks(self.config.truck_size)
        elif self.config.primary_tank_mode == PrimaryTankMode.OFF:
            # Never calculate primary tanks (preserve existing)
            pass
        elif (
            self.config.primary_tank_mode == PrimaryTankMode.AUTO
            and not self.has_any_primary_tanks()
        ):
            # Only calculate if no tanks are already marked as primary
            for site in self.sites:
                site.set_primary_tanks(self.config.truck_size)

        # Handle manifold/merged tank logic regardless of mode
        self._handle_manifold_primary_logic()

    @property
    def site_tanks(self) -> Iterable[tuple[Site, Tank]]:
        for site in self.sites:
            for tank in site.tanks:
                yield site, tank

    @property
    def primary_site_tanks(self) -> Iterable[tuple[Site, Tank]]:
        for site in self.sites:
            for tank in site.tanks:
                if tank.is_primary:
                    yield site, tank

    @property
    def secondary_site_tanks(self) -> Iterable[tuple[Site, Tank]]:
        for site in self.sites:
            for tank in site.tanks:
                if not tank.is_primary:
                    yield site, tank
