import logging

import casadi as ca

import numpy as np

from rtctools.optimization.single_pass_goal_programming_mixin import Goal

logger = logging.getLogger("mesido")
logger.setLevel(logging.INFO)


STANDARD_ASSET_LIFETIME = 30.0

default_assets_support = ["heat_source"]


class MinimizeVariableOPEX(Goal):
    order = 1

    def __init__(self, asset_types_supported=default_assets_support, year_step_size=10, priority=1):
        self.priority = priority
        self.year_step_size = year_step_size
        self.function_nominal = 1.0e6
        self.asset_types_supported = asset_types_supported

    def function(self, optimization_problem, ensemble_member):
        obj = 0
        asset_varopex_map = optimization_problem._asset_variable_operational_cost_map

        for asset_type in self.asset_types_supported:
            for asset in optimization_problem.energy_system_components.get(asset_type, []):
                extra_var = optimization_problem.extra_variable(asset_varopex_map.get(asset, 0.0))
                obj += extra_var * self.year_step_size

        return obj


class MaximizeRevenueCosts(Goal):

    order = 1

    def __init__(self, market_price=50e-6, year_step_size=10, priority=None):
        self.market_price = market_price  # [€/Wh]
        self.priority = priority
        self.year_step_size = year_step_size
        self.function_nominal = 1.0e6

    def function(self, optimization_problem, ensemble_member: int) -> ca.MX:
        obj = 0.0
        obj -= self.year_step_size * self.revenue_heat_delivered(
            optimization_problem, ensemble_member
        )

        return obj

    def revenue_heat_delivered(self, optimization_problem, ensemble_member: int) -> ca.MX:
        obj = 0
        timesteps = np.diff(optimization_problem.times()) / 3600.0
        for demand in optimization_problem.energy_system_components.get("heat_demand", []):
            obj += (
                optimization_problem.state(f"{demand}.Heat_demand") * timesteps * self.market_price
            )
        return obj


class MinimizeATESState(Goal):

    order = 1

    def __init__(self, priority=2):
        self.priority = priority

    def function(self, optimization_problem, ensemble_member):
        obj = 0.0
        for a in optimization_problem.energy_system_components.get("ates", []):
            obj += optimization_problem.state(f"{a}.Heat_loss")

        return obj / 1.0e6


class MinimizeCAPEXAssetsCosts(Goal):

    order = 1

    def __init__(self, asset_types_supported=default_assets_support, priority=None):
        self.priority = priority
        self.function_nominal = 1.0e6
        self.asset_types_supported = asset_types_supported

    def function(self, optimization_problem, ensemble_member):
        obj = self.capex_assets(optimization_problem, ensemble_member)
        return obj

    def capex_assets(self, optimization_problem, ensemble_member):
        # TODO: Check if cumulative capex costs can be used here.
        obj = 0.0
        parameters = optimization_problem.parameters(ensemble_member)
        bounds = optimization_problem.bounds()
        for asset_type in self.asset_types_supported:
            for asset_name in optimization_problem.energy_system_components.get(asset_type, []):
                tech_lifetime = parameters[f"{asset_name}.technical_life"]
                if asset_type == "heat_pipe":
                    is_placed = optimization_problem.get_asset_is__realized_symbols(asset_name)
                    investment_cost_coeff = bounds[f"{asset_name}__hn_cost"][1]

                    length = parameters[f"{asset_name}.length"]

                    costs = investment_cost_coeff * length
                else:
                    # Yearly depreciation for all assets based on total investment + installation
                    is_placed = optimization_problem.get_asset_fraction__placed_symbols(asset_name)
                    investment_cost_coeff = parameters[f"{asset_name}.investment_cost_coefficient"]
                    installation_cost = parameters[f"{asset_name}.installation_cost"]
                    size = bounds[optimization_problem._asset_max_size_map[asset_name]][1]
                    if size == 0:
                        raise RuntimeError(
                            f"Could not determine the max power of asset {asset_name}"
                        )
                    inv_costs = investment_cost_coeff * size
                    inst_costs = installation_cost

                    costs = inv_costs + inst_costs

                for i in range(optimization_problem._years):
                    obj += is_placed[i] * costs / tech_lifetime

        return obj * optimization_problem._year_step_size


class MinimizeRolloutFixedOperationalCosts(Goal):

    order = 1

    def __init__(self, years=25, asset_types_supported=default_assets_support, priority=1):
        self.priority = priority
        self.year_steps = years
        self.asset_types_supported = asset_types_supported if not None else ["heat_source"]

    def function(self, optimization_problem, ensemble_member: int) -> ca.MX:
        obj = 0.0
        obj += (
            optimization_problem._horizon
            / optimization_problem._years
            * self.fixed_operational_costs(optimization_problem, ensemble_member)
        )

        return obj / 1.0e6

    def fixed_operational_costs(self, optimization_problem, ensemble_member: int) -> ca.MX:
        obj = 0.0

        bounds = optimization_problem.bounds()
        parameters = optimization_problem.parameters(ensemble_member)
        for asset_type in self.asset_types_supported:
            if asset_type != "heat_pipe":
                for source in optimization_problem.energy_system_components.get(asset_type, []):
                    obj += self.fixed_opex_of_asset(
                        optimization_problem, source, bounds, parameters
                    )

        return obj

    def fixed_opex_of_asset(self, optimization_problem, asset_name, bounds, parameters):
        obj = 0

        is_placed = optimization_problem.get_asset_is__realized_symbols(asset_name)
        fixed_operational_cost_coeff = parameters[
            f"{asset_name}.fixed_operational_cost_coefficient"
        ]
        # fixed_operational_cost = optimization_problem._asset_fixed_operational_cost_map.get(
        #     asset_name)
        max_power = bounds[optimization_problem._asset_max_size_map[asset_name]][1]
        if max_power == 0:
            raise RuntimeError(f"Could not determine the max power of asset {asset_name}")
        # TODO: this can later be replaced by creating a new variable
        # {asset}_fix_operational_cost_{year} set equal to is_placed[i] *
        # _asset_fixed_operational_cost_map_var using bigm constraints.
        for i in range(optimization_problem._years):
            obj += is_placed[i] * fixed_operational_cost_coeff * max_power

        return obj
