import json
from pathlib import Path

import datamazing.pandas as pdz
import pandas as pd
import utm

CONNECTION_POINT_BEHIND_THE_METER = "installationstilsluttet (I)"


class MasterdataManager:

    SCHEMA = json.loads(
        (Path(__file__).parent / "schemas/masterdata_manager.json").read_bytes()
    )

    def __init__(
        self,
        db: pdz.Database,
        time_interval: pdz.TimeInterval = None,
        as_of_time: pd.Timestamp = None,
    ) -> None:
        self.db = db
        self.time_interval = time_interval
        self.as_of_time = as_of_time

        if as_of_time is not None and as_of_time.utcoffset().total_seconds() != 0:
            raise ValueError("'as_of_time' must be utc")

        if as_of_time is not None and (time_interval is not None):
            raise ValueError("Cannot provide both 'as_of_time' and 'time_interval'")
        if as_of_time is None and (time_interval is None):
            raise ValueError(
                "Either 'as_of_time' must be provided, or 'time_interval' must be"
                "provided"
            )

    @property
    def start_time(self) -> pd.Timestamp:
        if self.as_of_time:
            return self.as_of_time
        else:
            return self.time_interval.left

    @property
    def end_time(self) -> pd.Timestamp:
        if self.as_of_time:
            return self.as_of_time + pdz.get_epsilon(dtype=pd.DatetimeTZDtype(tz="UTC"))
        else:
            return self.time_interval.right

    @staticmethod
    def _intersect_time_and_comission_intervals(df: pd.DataFrame) -> pd.DataFrame:
        df = df.copy()
        # take max og start time and comission time (if latter is not null)
        df["start_time_utc"] = df[["start_time_utc", "commission_time_utc"]].max(axis=1)
        # take min of end time and decommission time (if latter is not null)
        df["end_time_utc"] = df[["end_time_utc", "decommission_time_utc"]].min(axis=1)
        # filter out rows where start time is after end time
        df = df[df["start_time_utc"] <= df["end_time_utc"]]

        df = df.drop(columns=["commission_time_utc", "decommission_time_utc"])
        return df

    def _filter_interval(self, df: pd.DataFrame) -> pd.DataFrame:
        df["start_time_utc"] = df["start_time_utc"].clip(lower=self.start_time)
        df["end_time_utc"] = df["end_time_utc"].clip(upper=self.end_time)
        df = df[df["start_time_utc"] < df["end_time_utc"]]
        return df

    def _get_table(self, table_name: str) -> pd.DataFrame:
        df = self.db.query(table_name)
        df = df.drop(columns=["emda_version", "created_time_utc"], errors="ignore")
        return df

    def get_plant_masterdata(self) -> pd.DataFrame:
        df_plant = self._get_table("masterdata_emda_plants")
        df_unit = self._get_table("masterdata_emda_units")
        df_market_participant = self._get_table("masterdata_emda_market_participants")

        df_plant = self._intersect_time_and_comission_intervals(df_plant)
        df_unit = self._intersect_time_and_comission_intervals(df_unit)

        df_plant = self._filter_interval(df_plant)
        df_unit = self._filter_interval(df_unit)
        df_market_participant = self._filter_interval(df_market_participant)

        df_unit = df_unit.sort_values(by="capacity_max_MW", ascending=False)

        df_unit_summary = pdz.group_interval(
            df_unit,
            by=["plant_id"],
            interval=("start_time_utc", "end_time_utc"),
        ).agg(
            {
                "power_system_resource_type": "first",  # From largest unit
                "asset_type": "first",  # From largest unit
                "power_system_resource": "first",  # From largest unit
                "coordinate_x_utm": "mean",
                "coordinate_y_utm": "mean",
                "hub_height_m": "mean",
                "c11": "first",  # Assuming unique per group
            }
        )

        df_plant = pdz.merge_interval_interval(
            df_plant,
            df_unit_summary,
            on=["plant_id"],
            interval=("start_time_utc", "end_time_utc"),
            how="left",
        )

        df_plant = pdz.merge_interval_interval(
            df_plant,
            df_market_participant,
            on=["market_participant_id"],
            interval=("start_time_utc", "end_time_utc"),
            how="left",
        )

        df_address = self._get_table("masterdata_address")
        df_address = df_address.rename(
            columns={
                "street_name": "address_street_name",
                "house_number": "address_house_number",
                "postal_code": "address_postal_code",
                "latitude": "address_latitude",
                "longitude": "address_longitude",
            }
        )

        df_plant = pdz.merge(
            df_plant,
            df_address,
            on=["address_street_name", "address_house_number", "address_postal_code"],
            how="left",
        )

        df_plant["is_household"] = is_household(df_plant)

        df_plant["latitude"], df_plant["longitude"] = coordinates(df_plant)
        df_plant = df_plant.drop(columns=["address_latitude", "address_longitude"])

        return df_plant


def is_household(df: pd.DataFrame) -> pd.Series:
    """
    Determine if a plant type corresponds to a household.
    """
    # due to inadequate quality of master data
    # we apply several filters to estimate
    # if an installation is a household or not,
    # based on the description found in
    # https://ens.dk/sites/ens.dk/files/Stoette_vedvarende_energi/energistyrelsens_vejledning_om_beregning_af_nettoafregning_og_opgoerelse_.pdf

    # household installations will be behind-the-meter
    # ("installationstilsluttet")
    is_behind_the_meter = df["connection_point"] == CONNECTION_POINT_BEHIND_THE_METER

    # household installations will be in the yearly
    # settlement group 6 ("årsbaseret nettoafregning")
    is_settlement_group_6 = df["settlement_group"] == 6

    # also remove installations with installed power
    # below 200 KW, as these will also most likely
    # be behind-the-meter installations (this should be
    # captured already in the above filters, but
    # masterdata is not fully reliable)
    # an example of a 200 KW plant can be found at coordinates 55.691, 9.397
    is_small = df["capacity_max_MW"] <= 0.2

    # if the installation is connected to the TSO,
    # it is definitely not a household installation
    is_tso_connected = df["operation_type"] == "Tso"

    return (is_behind_the_meter | is_settlement_group_6 | is_small) & ~is_tso_connected


def coordinates(
    df: pd.DataFrame,
) -> pd.DataFrame:
    # Translate UTM to lat/lon if they exist
    latitude, longitude = utm_to_latlon(
        df["coordinate_x_utm"],
        df["coordinate_y_utm"],
        df["price_area"],
    )

    # Prefer translated utm coordinates, fall back to address coordinates
    latitude = latitude.combine_first(df["address_latitude"])
    longitude = longitude.combine_first(df["address_longitude"])

    return latitude, longitude


def utm_to_latlon(
    x: pd.Series, y: pd.Series, price_area: pd.Series
) -> tuple[pd.Series, pd.Series]:
    """
    Convert UTM coordinates to latitude and longitude, given the price area.
    Ideally, we would use the zone number, but this is not available in masterdata
    currently. Instead, we use a workaround based on the price area, which should
    be sufficient for now. It follows the following logic:
    - If the price area is DK1, use UTM zone 32
    - If the price area is DK2, use UTM zone 33, unless the resulting longitude
      is above 16.0 (about 50 km to the right of Bornholm): In this case, we assume
      the real zone is actually 32, since no entities should be placed there.
    - If the price area is undefined, use UTM zone 32, since this is the majority of
      the area in Denmark.
    """
    if x.empty or y.empty or x.isnull().all() or y.isnull().all():
        return pd.Series(dtype=float), pd.Series(dtype=float)

    lat_32, lon_32 = utm.to_latlon(x, y, zone_number=32, northern=True, strict=False)

    lat_33, lon_33 = utm.to_latlon(x, y, zone_number=33, northern=True, strict=False)

    in_DK1 = price_area == "DK1"
    in_DK2 = price_area == "DK2"
    undefined = price_area.isnull()

    is_zone_32 = in_DK1 | undefined | (in_DK2 & (lon_33 >= 16.0))

    lat = lat_32.where(is_zone_32, lat_33)
    lon = lon_32.where(is_zone_32, lon_33)

    return lat, lon
