import logging
from collections import OrderedDict
from datetime import datetime, timezone
from textwrap import dedent
from typing import Any, Dict, List, Optional, Union

from sqlalchemy.orm import Session

from opennem.core.normalizers import normalize_duid
from opennem.core.units import get_unit
from opennem.db import get_database_engine
from opennem.db.models.opennem import FacilityScada, Station
from opennem.schema.network import NetworkSchema
from opennem.schema.time import TimeInterval, TimePeriod
from opennem.schema.units import UnitDefinition
from opennem.utils.cache import cache_scada_result
from opennem.utils.numbers import cast_trailing_nulls
from opennem.utils.time import human_to_timedelta
from opennem.utils.timezone import is_aware, make_aware
from opennem.utils.version import get_version

from .schema import (
    DataQueryResult,
    OpennemData,
    OpennemDataHistory,
    OpennemDataSet,
    ScadaDateRange,
)

logger = logging.getLogger(__name__)


def duid_in_case(facility_codes: List[str]) -> str:
    """Takes a list of duids and returns a formatted sql array for IN"""
    map_list = ["'{}'".format(i) for i in map(normalize_duid, facility_codes)]
    return ",".join(map_list)


def stats_factory(
    stats: List[DataQueryResult],
    units: UnitDefinition,
    interval: Optional[TimeInterval] = None,
    period: Optional[TimePeriod] = None,
    network: Optional[NetworkSchema] = None,
    timezone: Optional[Union[timezone, str]] = None,
    code: Optional[str] = None,
    region: Optional[str] = None,
    fueltech_group: Optional[bool] = False,
    group_field: Optional[str] = None,
    data_id: Optional[str] = None,
) -> Optional[OpennemDataSet]:
    """
    Takes a list of data query results and returns OpennemDataSets

    @TODO optional groupby field
    @TODO override timezone
    @TODO multiple groupings / slight refactor

    """

    if network:
        timezone = network.get_timezone()

    dates = [s.interval for s in stats]

    if not dates:
        return None

    start = min(dates)
    end = max(dates)

    # should probably make sure these are the same TZ
    if timezone and not is_aware(start):
        start = make_aware(min(dates), timezone)

    if timezone and not is_aware(end):
        end = make_aware(max(dates), timezone)

    # free
    dates = []

    group_codes = list(set([i.group_by for i in stats if i.group_by]))

    stats_grouped = []

    for group_code in group_codes:

        data_grouped: Dict[Any, Any] = dict()

        for stat in stats:
            if stat.interval not in data_grouped:
                data_grouped[stat.interval] = None

            if stat.group_by == group_code:
                data_grouped[stat.interval] = stat.result

        data_sorted = OrderedDict(sorted(data_grouped.items()))

        data_value = list(data_sorted.values())

        # Skip null series
        if len([i for i in data_value if i]) == 0:
            continue

        # Skip zero series
        if sum([i for i in data_value if i]) == 0:
            continue

        # Cast trailing nulls
        if not units.name.startswith("temperature") or units.cast_nulls:
            data_value = cast_trailing_nulls(data_value)

        history = OpennemDataHistory(
            start=start,
            last=end,
            data=data_value,
        )

        if interval:
            history.interval = interval.interval_human

        data = OpennemData(
            data_type=units.unit_type,
            units=units.unit,
            code=group_code,
            # interval=interval,
            # period=period,
            history=history,
        )

        if network:
            data.network = network.code

        # *sigh* - not the most flexible model
        # @TODO fix this schema and make it more flexible
        if fueltech_group:
            data.fuel_tech = group_code
            data_comps = [
                # @NOTE disable for now since FE doesn't
                # support it
                # network.country if network else None,
                network.code.lower() if network else None,
                region.lower() if region else None,
                "fuel_tech",
                group_code.lower(),
                units.unit_type,
            ]
            data.id = ".".join(i for i in data_comps if i)
            # @TODO make this an alias
            data.type = units.unit_type

        if group_field:
            group_fields = []

            # setattr(data, group_field, group_code)

            if network:
                group_fields.append(network.code.lower())

            group_fields = group_fields + [
                group_field,
                group_code,
                units.unit_type,
            ]

            data.id = ".".join([f for f in group_fields if f])
            data.type = units.unit_type

        if data_id:
            data.id = data_id

        if not data.id:
            _id_list = []

            if region:
                _id_list.append(region.lower())

            elif network:
                _id_list.append(network.code)

            if units and units.name_alias:
                _id_list.append(units.name_alias)

            data.id = ".".join([f.lower() for f in _id_list if f])
            data.type = units.unit_type

        if region:
            data.region = region

        stats_grouped.append(data)

    dt_now = datetime.now()

    if network:
        dt_now = dt_now.astimezone(network.get_timezone())

    # @NOTE this should probably be
    # country.network.region
    if not code:
        if network:
            code = network.code.lower()

        if region:
            code = region.lower()

    stat_set = OpennemDataSet(
        type=units.unit_type,
        data=stats_grouped,
        created_at=dt_now,
        code=code,
        version=get_version(),
    )

    if network:
        stat_set.network = network.code

    if region:
        stat_set.region = region

    return stat_set


def networks_to_in(networks: List[NetworkSchema]) -> str:
    codes = ["'{}'".format(n.code) for n in networks]

    return ", ".join(codes)


SCADA_RANGE_EXCLUDE_DUIDS = [
    "V-SA",
    "V-SA-2",
    "N-Q-MNSP1",
    "N-Q-MNSP1-2",
    "NSW1-QLD1",
    "NSW1-QLD1-2",
    "V-S-MNSP1",
    "V-S-MNSP1-2",
    "T-V-MNSP1",
    "T-V-MNSP1-2",
    "VIC1-NSW1",
    "VIC1-NSW1-2",
]


@cache_scada_result
def get_scada_range(
    network: Optional[NetworkSchema] = None,
    networks: Optional[List[NetworkSchema]] = None,
    network_region: Optional[str] = None,
    facilities: Optional[List[str]] = None,
) -> Optional[ScadaDateRange]:
    """Get the start and end dates for a network query. This is more efficient
    than providing or querying the range at query time
    """
    engine = get_database_engine()

    __query = """
        select
            min(fs.trading_interval)::timestamp AT TIME ZONE '{timezone}',
            max(fs.trading_interval)::timestamp AT TIME ZONE '{timezone}'
        from facility_scada fs
        where
            {facility_query}
            {network_query}
            {network_region_query}
            facility_code not like 'ROOFTOP_%%'
            and facility_code not in ({exclude_duids})
    """

    network_query = ""
    timezone = "UTC"

    if network:
        network_query = f"fs.network_id = '{network.code}' and"
        # timezone = network.timezone_database

    if networks:
        net_case = networks_to_in(networks)
        network_query = "fs.network_id IN ({}) and ".format(net_case)

    network_region_query = ""

    if network_region:
        # @TODO support network regions in scada_range
        # technically we don't need this atm
        pass
        # network_region_query = (
        # f"f.network_region = '{network_region.code}' and"
        # )

    facility_query = ""

    if facilities:
        fac_case = duid_in_case(facilities)
        facility_query = "fs.facility_code IN ({}) and ".format(fac_case)

    exclude_duids = duid_in_case(SCADA_RANGE_EXCLUDE_DUIDS)

    scada_range_query = dedent(
        __query.format(
            facility_query=facility_query,
            network_query=network_query,
            network_region_query=network_region_query,
            timezone=timezone,
            exclude_duids=exclude_duids,
        )
    )

    with engine.connect() as c:
        logger.debug(scada_range_query)
        scada_range_result = list(c.execute(scada_range_query))

        if len(scada_range_result) < 1:
            raise Exception("No scada range result")

        scada_min = scada_range_result[0][0]
        scada_max = scada_range_result[0][1]

    if not scada_min or not scada_max:
        return None

    scada_range = ScadaDateRange(start=scada_min, end=scada_max, network=network)

    return scada_range


def station_attach_stats(station: Station, session: Session) -> Station:
    # @TODO update for new queries
    since = datetime.now() - human_to_timedelta("7d")

    facility_codes = list(set([f.code for f in station.facilities]))

    stats = (
        session.query(FacilityScada)
        .filter(FacilityScada.facility_code.in_(facility_codes))
        .filter(FacilityScada.trading_interval >= since)
        .order_by(FacilityScada.facility_code)
        .order_by(FacilityScada.trading_interval)
        .all()
    )

    for facility in station.facilities:
        facility_power = list(filter(lambda s: s.facility_code == facility.code, stats))

        # facility.scada_power = stats_factory(facility_power)

    return station
