import uuid
from enum import Enum
from typing import Optional, Literal, Self, List

from pydantic import BaseModel, computed_field, model_validator, Field

from bb_integrations_lib.shared.model import AgGridBaseModel
from bb_integrations_lib.util.utils import is_valid_goid, is_uuid


class Point(BaseModel):
    lat: float
    lon: float


class CrossroadsEntityType(str, Enum):
    """Describes the type of entity used for crossroads. These are independent of Supply & Dispatch's model. At present
    it's a pretty close match but they will diverge more as more crossroads features are added."""
    site = "site",
    tank = "tank",
    terminal = "terminal",
    product = "product",
    company = "company"


class BaseCrossroadsEntity(AgGridBaseModel):
    """Base Crossroads Entity. All entities have an is_active switch for soft deletion and a GOID for unique identification.
        All entities need a grid. They all need to extend AgGridBaseModel.
    """
    goid: str = Field(..., description="Gravitate Object ID of the record. This field is autogenerated, unique, and immutable.", frozen=True)
    is_active: bool = Field(True)
    record_owner: Optional[str] = Field(None, description="GOID of the company that owns this record. May be null.")


class CrossroadsCompany(BaseCrossroadsEntity):
    """A Compnay is any business entity. It forms the backbone of the relationship network and the integration network and is a component of the entity network."""
    name: str = Field(..., description="Name of the company")
    description: Optional[str] = Field(None, description="Details")
    is_gravitate_customer: bool = Field(False, description="Does this company use at least one Gravitate product?")
    rita_tenant: Optional[str] = Field(None, description="The tenant this company is associated with, if any. Typically only exists for Gravitate customers.")
    ein: Optional[str] = Field(None, description="The Federal Employer Identification Number of the company.")
    is_retailer: bool = Field(False, description="True if the company is ever a retailer.")
    is_wholesaler: bool = Field(False, description="True if the company is ever a wholesaler.")
    is_carrier: bool = Field(False, description="True if the company is ever a carrier.")
    is_supplier: bool = Field(False, description="True if the company is ever a supplier.")
    is_dealer: bool = Field(False, description="True if the company is ever a dealer.")

    def flatten(self) -> dict:
        obj = self.model_dump(mode="json", exclude={"is_active"})
        return obj

    @classmethod
    def unflatten(cls, flattened: dict) -> Self:
        goid = flattened.get("goid") or str(uuid.uuid4())
        return cls.model_validate(flattened | {"goid": goid})

    @model_validator(mode="after")
    def validate_goid(self):
        if not is_valid_goid(self.goid, "company") and not is_uuid(self.goid):
            raise ValueError("GOID must be in the form `company:<number>`")
        return self


class ProductType(str, Enum):
    """Top-level type for products"""
    gas = "gas",
    diesel = "diesel",
    jet = "jet",
    ethanol = "ethanol",
    bio = "bio"


class ProductSubtype(str, Enum):
    """Subtype for products. Some of these are only valid for certain product types. This is enforced by a validator on the Product entity."""
    rbob = "RBOB",
    cbob = "CBOB",
    carb = "CARBOB",
    ulsd = "ULSD"


gas_subtypes = [ProductSubtype.rbob, ProductSubtype.cbob, ProductSubtype.carb]
diesel_subtypes = [ProductSubtype.ulsd]


class CrossroadsProduct(BaseCrossroadsEntity):
    """A fully-described product in the crossroads network."""
    type: ProductType = Field(..., description="Type of product")
    subtype: ProductSubtype = Field(..., description="Subtype of product")
    name: str = Field(..., description="Name of the product")
    gas_octane: Optional[int] = Field(None, description="Octane rating. type == gas only.")
    gas_ethanol: Optional[int] = Field(None, description="Ethanol component as a percentage. type == gas only.")
    gas_rvp: Optional[float] = Field(None, description="Reid Vapor Pressure. type == gas only.")
    gas_formulation: Optional[Literal["Conv", "RFG"]] = Field(None, description="Formulation of the gas. type == gas only.")
    diesel_no: Optional[int] = Field(None, description="Diesel formulation number. type == diesel only.")
    diesel_bio: Optional[int] = Field(None, description="Diesel bio component as a percentage. type == diesel only.")
    diesel_winter: Optional[bool] = Field(None, description="Whether this is a winter diesel blend. type == diesel only.")
    diesel_dyed: Optional[bool] = Field(None, description="Whether this is a dyed diesel product. type == diesel only.")
    diesel_additive: Optional[bool] = Field(None, description="Whether this product has additives. type == diesel only.")
    # More properties planned for other product types

    @computed_field
    @property
    def description(self) -> str:
        if self.type == "gas":
            return f"Gasoline {self.subtype.value} {self.gas_octane} E{self.gas_ethanol} {self.gas_rvp} RVP {self.gas_formulation}"
        if self.type == "diesel":
            return f"Diesel {self.subtype.value} #{self.diesel_no} B{self.diesel_bio} {"winter blend" if self.diesel_winter else "summer blend"} {"dyed" if self.diesel_dyed else "clear"} {"w/ additive" if self.diesel_additive else ""}"
        return ""

    def flatten(self) -> dict:
        obj = self.model_dump(mode="json", exclude={"is_active"})
        return obj

    @classmethod
    def unflatten(cls, flattened: dict) -> Self:
        goid = flattened.get("goid") or str(uuid.uuid4())
        return cls.model_validate(flattened | {"goid": goid})

    @model_validator(mode="after")
    def validate_fields(self):
        if self.type == "gas":
            if self.gas_octane is None or self.gas_ethanol is None or self.gas_rvp is None or self.gas_formulation is None:
                raise ValueError("Gas product must have gas_octane, gas_ethanol, gas_rvp, and gas_formulation fields")
            if self.diesel_no is not None or self.diesel_bio is not None or self.diesel_winter is not None or self.diesel_dyed is not None or self.diesel_additive is not None:
                raise ValueError(
                    "Gas product cannot have diesel_no, diesel_bio, diesel_winter, diesel_dyed, or diesel_additive fields")
            if self.subtype not in gas_subtypes:
                raise ValueError(f"Gas product must have one of the following subtypes: {gas_subtypes}")
        if self.type == "diesel":
            if self.diesel_no is None or self.diesel_bio is None or self.diesel_winter is None or self.diesel_dyed is None or self.diesel_additive is None:
                raise ValueError(
                    "Diesel product must have diesel_no, diesel_bio, diesel_winter, diesel_dyed, and diesel_additive fields")
            if self.gas_octane is not None or self.gas_ethanol is not None or self.gas_rvp is not None or self.gas_formulation is not None:
                raise ValueError("Diesel product cannot have gas_octane, gas_ethanol, gas_rvp, or gas_formulation fields")
            if self.subtype not in diesel_subtypes:
                raise ValueError(f"Diesel product must have one of the following subtypes: {diesel_subtypes}")
        return self


class SiteStatus(str, Enum):
    open = "open",
    closed = "closed"


class CrossroadsSite(BaseCrossroadsEntity):
    """Describes a site in the crossroads network."""
    federal_site_id: Optional[str] = Field(None, description="Facility ID from the US EPA, if available.")
    name: str = Field(..., description="Name of the site")
    address: str = Field(..., description="Street address of the site")
    city: str = Field(..., description="City")
    state: str = Field(..., description="State (not abbreviated)")
    country: str = Field(..., description="Country (not abbreviated)")
    postal_code: str = Field(..., description="Postal code")
    location: Point = Field(..., description="Geographic location of the site")
    geofence: Optional[List[Point]] = Field(None, description="A list of points that form a polygon defining the geofence for this site. Points should be in order, with the first and last points being the same.", )
    status: SiteStatus = Field("open", description="Whether the site is open or closed")

    def flatten(self) -> dict:
        obj = self.model_dump(mode="json", exclude={"is_active", "location", "geofence"})
        obj["lat"] = self.location.lat
        obj["lon"] = self.location.lon
        return obj

    @classmethod
    def unflatten(cls, flattened: dict) -> Self:
        location = {"lat": flattened.get("lat", 0), "lon": flattened.get("lon", 0)}
        goid = flattened.get("goid") or str(uuid.uuid4())
        return cls.model_validate(flattened | {"location": location, "goid": goid})

    @model_validator(mode="after")
    def validate_goid(self):
        if not is_valid_goid(self.goid, "site") and not is_uuid(self.goid):
            raise ValueError("goid must be in the form `site:<number>`")
        if self.record_owner is not None and not is_valid_goid(self.record_owner, "company"):
            raise ValueError("record_owner must be a GOID in the form `company:<number>`")
        return self

    @model_validator(mode="after")
    def validate_geofence(self):
        if self.geofence is None:
            return self
        if len(self.geofence) < 3:
            raise ValueError("Geofence must contain at least 3 points")
        if self.geofence[0] != self.geofence[-1]:
            raise ValueError("First and last points of geofence must be the same")
        return self


class TankStatus(str, Enum):
    open = "open",
    closed = "closed",
    transitioning = "transitioning"


class CrossroadsTank(BaseCrossroadsEntity):
    """Describes a tank in the crossroads network."""
    tank_id: str = Field(..., description="ID of the tank. Must be unique within the site.")
    federal_tank_id: Optional[str] = Field(None, description="Federal Tank ID, if available.")
    inventory_manager: Optional[str] = Field(None, description="GOID of the company that manages the inventory for this tank. If it is null, the inventory manager is unknown.")
    site: str = Field(..., description="The site that this tank belongs to.")
    product: str = Field(..., description="ID of the product that this tank is for.")
    brand: Literal["Branded", "Unbranded"] = Field(..., description="Whether this tank has a branded or unbranded product.")
    tank_size: int = Field(..., description="Volume of the tank in gallons.")
    status: TankStatus = Field("open", description="Status of the tank. If 'transitioning', see the transitioning fields for details.")
    transitioning_to: Optional[str] = Field(None, description="GOID of the product that this tank is transitioning to. If null, this tank is not transitioning.")
    transitioning_from: Optional[str] = Field(None, description="GOID of the product that this tank is transitioning from. If null, this tank is not transitioning.")
    storage_max: Optional[int] = Field(..., description="Maximum storage capacity of the tank in gallons.")
    fuel_bottom: Optional[int] = Field(..., description="Fuel bottom of the tank in gallons.")

    @computed_field
    @property
    def name(self) -> str:
        return f"Tank {self.tank_id} - {self.product}"

    def flatten(self) -> dict:
        obj = self.model_dump(mode="json", exclude={"is_active"})
        return obj

    @classmethod
    def unflatten(cls, flattened: dict) -> Self:
        goid = flattened.get("goid") or str(uuid.uuid4())
        return cls.model_validate(flattened | {"goid": goid})

    @model_validator(mode="after")
    def validate(self):
        if not is_valid_goid(self.goid, "tank") and not is_uuid(self.goid):
            raise ValueError("goid must be a GOID in the form `tank:<number>`")
        if self.record_owner is not None and not is_valid_goid(self.record_owner, "company"):
            raise ValueError("record_owner must be a GOID in the form `company:<number>`")
        if self.inventory_manager is not None and not is_valid_goid(self.inventory_manager, "company"):
            raise ValueError("inventory_manager must be a GOID in the form `company:<number>`")
        if not is_valid_goid(self.site, "site"):
            raise ValueError("site must be a GOID in the form `site:<number>`")
        if not is_valid_goid(self.product, "product"):
            raise ValueError("product must be a GOID in the form `product:<number>`")
        if self.status == "transitioning" and (self.transitioning_to is None and self.transitioning_from is None):
            raise ValueError("If status is transitioning, transitioning_to_product_id and transitioning_from_product_id must be set")
        if self.transitioning_to is not None and not is_valid_goid(self.transitioning_to, "product"):
            raise ValueError("transitioning_to must be a GOID in the form `product:<number>`")
        if self.transitioning_from is not None and not is_valid_goid(self.transitioning_from, "product"):
            raise ValueError("transitioning_from must be a GOID in the form `product:<number>`")
        return self


class CrossroadsTerminal(BaseCrossroadsEntity):
    """Describes a terminal in the Crossroads Network"""
    tcn: Optional[str] = Field(None, description="Federal Terminal ID. This should be set for most terminals.")
    alternate_id: Optional[str] = Field(None, description="Alternate ID for this terminal. This must be set if tcn is not set.")
    name: str = Field(..., description="Name of the terminal")
    address: str = Field(..., description="Street address of the terminal")
    city: str = Field(..., description="City")
    state: str = Field(..., description="State (not abbreviated)")
    country: str = Field(..., description="Country (not abbreviated)")
    postal_code: str = Field(..., description="Postal code")
    location: Point = Field(..., description="Geographic location of the terminal")
    geofence: Optional[List[Point]] = Field(None, description="A list of points that form a polygon defining the geofence for this terminal. Points should be in order, with the first and last points being the same.", )
    terminal_owner: Optional[str] = Field(None, description="GOID of the company that owns this terminal. If it is null, no link has been made between this record and a Company record.")
    products: List[str] = Field([], description="List of product GOIDs that this terminal can supply.")
    suppliers: List[str] = Field([], description="List of company GOIDs for suppliers that are available at this terminal.")

    def flatten(self) -> dict:
        obj = self.model_dump(mode="json", exclude={"is_active", "location", "geofence", "products", "suppliers"})
        obj["lat"] = self.location.lat
        obj["lon"] = self.location.lon
        return obj

    @classmethod
    def unflatten(cls, flattened: dict) -> Self:
        location = {"lat": flattened.get("lat", 0), "lon": flattened.get("lon", 0)}
        goid = flattened.get("goid") or str(uuid.uuid4())
        return cls.model_validate(flattened | {"location": location, "goid": goid})

    @model_validator(mode="after")
    def validate(self):
        if (self.tcn is None and self.alternate_id is None) or (self.tcn is not None and self.alternate_id is not None):
            raise ValueError("Terminal must have either a tcn or an alternate_id, but not both.")
        if not is_valid_goid(self.goid, "terminal") and not is_uuid(self.goid):
            raise ValueError("goid must be in the form `terminal:<number>`")
        if self.record_owner is not None and not is_valid_goid(self.record_owner, "company"):
            raise ValueError("reco:rd_owner must be a GOID in the form `company:<number>`")
        if self.terminal_owner is not None and not is_valid_goid(self.terminal_owner, "company"):
            raise ValueError("terminal_owner must be a GOID in the form `company:<number>`")
        return self

    @model_validator(mode="after")
    def validate_geofence(self):
        if self.geofence is None:
            return self
        if len(self.geofence) < 3:
            raise ValueError("Geofence must contain at least 3 points")
        if self.geofence[0] != self.geofence[-1]:
            raise ValueError("First and last points of geofence must be the same")
        return self
