import csv
import os
import sys
import threading
import traceback

from dateutil.relativedelta import relativedelta

from flask import g, request

from sqlalchemy import or_
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import func, null

import chellow.dloads
from chellow.models import (
    Batch,
    Bill,
    Era,
    GeneratorType,
    MeasurementRequirement,
    Mtc,
    ReadType,
    RegisterRead,
    Session,
    SiteEra,
    Supply,
)
from chellow.utils import (
    CHANNEL_TYPES,
    HH,
    csv_make_val,
    hh_format,
    hh_min,
    parse_mpan_core,
    req_date,
    req_int,
    req_str,
)
from chellow.views import chellow_redirect


def content(running_name, finished_name, date, supply_id, mpan_cores):
    sess = None
    try:
        sess = Session()
        f = open(running_name, mode="w", newline="")
        writer = csv.writer(f, lineterminator="\n")
        titles = (
            "Date",
            "Import MPAN Core",
            "Export MPAN Core",
            "Physical Site Id",
            "Physical Site Name",
            "Other Site Ids",
            "Other Site Names",
            "Supply Id",
            "Source",
            "Generator Type",
            "GSP Group",
            "DNO Name",
            "Voltage Level",
            "Is Substations",
            "Metering Type",
            "Mandatory HH",
            "PC",
            "MTC",
            "CoP",
            "Comms Type",
            "SSC Code",
            "SSC Description",
            "Energisation Status",
            "Number Of Registers",
            "MOP Contract",
            "Mop Account",
            "DC Contract",
            "DC Account",
            "Meter Serial Number",
            "Meter Installation Date",
            "Latest Normal Meter Read Date",
            "Latest Normal Meter Read Type",
            "Latest DC Bill Date",
            "Latest MOP Bill Date",
            "Supply Start Date",
            "Supply Finish Date",
            "Properties",
            "Import ACTIVE?",
            "Import REACTIVE_IMPORT?",
            "Import REACTIVE_EXPORT?",
            "Export ACTIVE?",
            "Export REACTIVE_IMPORT?",
            "Export REACTIVE_EXPORT?",
            "Import Agreed Supply Capacity (kVA)",
            "Import LLFC Code",
            "Import LLFC Description",
            "Import Supplier Contract",
            "Import Supplier Account",
            "Import Mandatory kW",
            "Latest Import Supplier Bill Date",
            "Export Agreed Supply Capacity (kVA)",
            "Export LLFC Code",
            "Export LLFC Description",
            "Export Supplier Contract",
            "Export Supplier Account",
            "Export Mandatory kW",
            "Latest Export Supplier Bill Date",
        )
        writer.writerow(titles)

        NORMAL_READ_TYPES = ("N", "C", "N3")
        year_start = date + HH - relativedelta(years=1)

        era_ids = (
            sess.query(Era.id)
            .filter(
                Era.start_date <= date,
                or_(Era.finish_date == null(), Era.finish_date >= date),
            )
            .order_by(Era.supply_id)
        )

        if supply_id is not None:
            supply = Supply.get_by_id(sess, supply_id)

            era_ids = era_ids.filter(Era.supply == supply)

        if mpan_cores is not None:
            era_ids = era_ids.filter(
                or_(
                    Era.imp_mpan_core.in_(mpan_cores), Era.exp_mpan_core.in_(mpan_cores)
                )
            )

        for (era_id,) in era_ids:

            era, supply, generator_type = (
                sess.query(Era, Supply, GeneratorType)
                .join(Supply, Era.supply_id == Supply.id)
                .outerjoin(GeneratorType, Supply.generator_type_id == GeneratorType.id)
                .filter(Era.id == era_id)
                .options(
                    joinedload(Era.channels),
                    joinedload(Era.cop),
                    joinedload(Era.comm),
                    joinedload(Era.dc_contract),
                    joinedload(Era.exp_llfc),
                    joinedload(Era.exp_supplier_contract),
                    joinedload(Era.imp_llfc),
                    joinedload(Era.imp_supplier_contract),
                    joinedload(Era.mop_contract),
                    joinedload(Era.mtc),
                    joinedload(Era.mtc).joinedload(Mtc.meter_type),
                    joinedload(Era.pc),
                    joinedload(Era.site_eras).joinedload(SiteEra.site),
                    joinedload(Era.ssc),
                    joinedload(Era.energisation_status),
                    joinedload(Era.supply).joinedload(Supply.source),
                    joinedload(Era.supply).joinedload(Supply.gsp_group),
                    joinedload(Era.supply).joinedload(Supply.dno),
                )
                .one()
            )

            site_codes = []
            site_names = []
            for site_era in era.site_eras:
                if site_era.is_physical:
                    physical_site = site_era.site
                else:
                    site = site_era.site
                    site_codes.append(site.code)
                    site_names.append(site.name)

            sup_eras = (
                sess.query(Era)
                .filter(Era.supply == supply)
                .order_by(Era.start_date)
                .all()
            )
            supply_start_date = sup_eras[0].start_date
            supply_finish_date = sup_eras[-1].finish_date

            if era.imp_mpan_core is None:
                voltage_level_code = era.exp_llfc.voltage_level.code
                is_substation = era.exp_llfc.is_substation
            else:
                voltage_level_code = era.imp_llfc.voltage_level.code
                is_substation = era.imp_llfc.is_substation

            if generator_type is None:
                generator_type_str = ""
            else:
                generator_type_str = generator_type.code

            metering_type = era.meter_category

            if metering_type in ("nhh", "amr"):
                latest_prev_normal_read = (
                    sess.query(RegisterRead)
                    .join(Bill)
                    .join(RegisterRead.previous_type)
                    .filter(
                        ReadType.code.in_(NORMAL_READ_TYPES),
                        RegisterRead.previous_date <= date,
                        Bill.supply_id == supply.id,
                    )
                    .order_by(RegisterRead.previous_date.desc())
                    .options(joinedload(RegisterRead.previous_type))
                    .first()
                )

                latest_pres_normal_read = (
                    sess.query(RegisterRead)
                    .join(Bill)
                    .join(RegisterRead.present_type)
                    .filter(
                        ReadType.code.in_(NORMAL_READ_TYPES),
                        RegisterRead.present_date <= date,
                        Bill.supply == supply,
                    )
                    .order_by(RegisterRead.present_date.desc())
                    .options(joinedload(RegisterRead.present_type))
                    .first()
                )

                if latest_prev_normal_read is None and latest_pres_normal_read is None:
                    latest_normal_read_date = None
                    latest_normal_read_type = None
                elif (
                    latest_pres_normal_read is not None
                    and latest_prev_normal_read is None
                ):
                    latest_normal_read_date = latest_pres_normal_read.present_date
                    latest_normal_read_type = latest_pres_normal_read.present_type.code
                elif (
                    latest_pres_normal_read is None
                    and latest_prev_normal_read is not None
                ):
                    latest_normal_read_date = latest_prev_normal_read.previous_date
                    latest_normal_read_type = latest_prev_normal_read.previous_type.code
                elif (
                    latest_pres_normal_read.present_date
                    > latest_prev_normal_read.previous_date
                ):
                    latest_normal_read_date = latest_pres_normal_read.present_date
                    latest_normal_read_type = latest_pres_normal_read.present_type.code
                else:
                    latest_normal_read_date = latest_prev_normal_read.previous_date
                    latest_normal_read_type = latest_prev_normal_read.previous_type.code
                if latest_normal_read_date is not None:
                    latest_normal_read_date = hh_format(latest_normal_read_date)

            else:
                latest_normal_read_date = metering_type
                latest_normal_read_type = None

            mop_contract = era.mop_contract
            mop_contract_name = mop_contract.name
            mop_account = era.mop_account
            latest_mop_bill_date = (
                sess.query(Bill.finish_date)
                .join(Batch)
                .filter(
                    Bill.start_date <= date,
                    Bill.supply == supply,
                    Batch.contract == mop_contract,
                )
                .order_by(Bill.finish_date.desc())
                .first()
            )

            if latest_mop_bill_date is not None:
                latest_mop_bill_date = hh_format(latest_mop_bill_date[0])

            dc_contract = era.dc_contract
            dc_contract_name = dc_contract.name
            dc_account = era.dc_account
            latest_dc_bill_date = (
                sess.query(Bill.finish_date)
                .join(Batch)
                .filter(
                    Bill.start_date <= date,
                    Bill.supply == supply,
                    Batch.contract == dc_contract,
                )
                .order_by(Bill.finish_date.desc())
                .first()
            )

            if latest_dc_bill_date is not None:
                latest_dc_bill_date = hh_format(latest_dc_bill_date[0])

            channel_values = []
            for imp_related in [True, False]:
                for channel_type in CHANNEL_TYPES:
                    if era.find_channel(sess, imp_related, channel_type) is None:
                        channel_values.append("false")
                    else:
                        channel_values.append("true")

            imp_avg_months = None
            exp_avg_months = None
            for is_import in [True, False]:
                if metering_type == "nhh":
                    continue

                params = {
                    "supply_id": supply.id,
                    "year_start": year_start,
                    "year_finish": date,
                    "is_import": is_import,
                }
                month_mds = tuple(
                    md[0] * 2
                    for md in sess.execute(
                        """

    select max(hh_datum.value) as md
    from hh_datum join channel on (hh_datum.channel_id = channel.id)
        join era on (channel.era_id = era.id)
    where era.supply_id = :supply_id and hh_datum.start_date >= :year_start
        and hh_datum.start_date <= :year_finish
        and channel.channel_type = 'ACTIVE'
        and channel.imp_related = :is_import
    group by extract(month from (hh_datum.start_date at time zone 'utc'))
    order by md desc
    limit 3

    """,
                        params=params,
                    )
                )

                avg_months = sum(month_mds)
                if len(month_mds) > 0:
                    avg_months /= len(month_mds)
                    if is_import:
                        imp_avg_months = avg_months
                    else:
                        exp_avg_months = avg_months

            if (imp_avg_months is not None and imp_avg_months > 100) or (
                exp_avg_months is not None and exp_avg_months > 100
            ):
                mandatory_hh = "yes"
            else:
                mandatory_hh = "no"

            imp_latest_supplier_bill_date = None
            exp_latest_supplier_bill_date = None
            for is_import in (True, False):
                for er in (
                    sess.query(Era)
                    .filter(Era.supply == era.supply, Era.start_date <= date)
                    .order_by(Era.start_date.desc())
                ):
                    if is_import:
                        if er.imp_mpan_core is None:
                            break
                        else:
                            supplier_contract = er.imp_supplier_contract
                    else:
                        if er.exp_mpan_core is None:
                            break
                        else:
                            supplier_contract = er.exp_supplier_contract

                    latest_bill_date = (
                        sess.query(Bill.finish_date)
                        .join(Batch)
                        .filter(
                            Bill.finish_date >= er.start_date,
                            Bill.finish_date <= hh_min(er.finish_date, date),
                            Bill.supply == supply,
                            Batch.contract == supplier_contract,
                        )
                        .order_by(Bill.finish_date.desc())
                        .first()
                    )

                    if latest_bill_date is not None:
                        latest_bill_date = hh_format(latest_bill_date[0])

                        if is_import:
                            imp_latest_supplier_bill_date = latest_bill_date
                        else:
                            exp_latest_supplier_bill_date = latest_bill_date
                        break

            meter_installation_date = (
                sess.query(func.min(Era.start_date))
                .filter(Era.supply == era.supply, Era.msn == era.msn)
                .one()[0]
            )

            ssc = era.ssc
            if ssc is None:
                ssc_code = ssc_description = num_registers = None
            else:
                ssc_code, ssc_description = ssc.code, ssc.description
                num_registers = (
                    sess.query(MeasurementRequirement)
                    .filter(MeasurementRequirement.ssc == ssc)
                    .count()
                )

            vals = (
                [
                    date,
                    era.imp_mpan_core,
                    era.exp_mpan_core,
                    physical_site.code,
                    physical_site.name,
                    ", ".join(site_codes),
                    ", ".join(site_names),
                    supply.id,
                    supply.source.code,
                    generator_type_str,
                    supply.gsp_group.code,
                    supply.dno.dno_code,
                    voltage_level_code,
                    is_substation,
                    metering_type,
                    mandatory_hh,
                    era.pc.code,
                    era.mtc.code,
                    era.cop.code,
                    era.comm.code,
                    ssc_code,
                    ssc_description,
                    era.energisation_status.code,
                    num_registers,
                    mop_contract_name,
                    mop_account,
                    dc_contract_name,
                    dc_account,
                    era.msn,
                    meter_installation_date,
                    latest_normal_read_date,
                    latest_normal_read_type,
                    latest_dc_bill_date,
                    latest_mop_bill_date,
                    supply_start_date,
                    supply_finish_date,
                    era.properties,
                ]
                + channel_values
                + [
                    era.imp_sc,
                    None if era.imp_llfc is None else era.imp_llfc.code,
                    None if era.imp_llfc is None else era.imp_llfc.description,
                    None
                    if era.imp_supplier_contract is None
                    else era.imp_supplier_contract.name,
                    era.imp_supplier_account,
                    imp_avg_months,
                    imp_latest_supplier_bill_date,
                ]
                + [
                    era.exp_sc,
                    None if era.exp_llfc is None else era.exp_llfc.code,
                    None if era.exp_llfc is None else era.exp_llfc.description,
                    None
                    if era.exp_supplier_contract is None
                    else era.exp_supplier_contract.name,
                    era.exp_supplier_account,
                    exp_avg_months,
                    exp_latest_supplier_bill_date,
                ]
            )
            writer.writerow([csv_make_val(v) for v in vals])

            # Avoid a long-running transaction
            sess.rollback()
    except BaseException:
        msg = traceback.format_exc()
        sys.stderr.write(msg)
        writer.writerow([msg])
    finally:
        if sess is not None:
            sess.close()
        if f is not None:
            f.close()
            os.rename(running_name, finished_name)


def do_get(session):
    user = g.user
    date = req_date("date")
    if "supply_id" in request.values:
        supply_id = req_int("supply_id")
    else:
        supply_id = None

    if "mpan_cores" in request.values:
        mpan_cores_str = req_str("mpan_cores")
        mpan_cores = mpan_cores_str.splitlines()
        if len(mpan_cores) == 0:
            mpan_cores = None
        else:
            for i in range(len(mpan_cores)):
                mpan_cores[i] = parse_mpan_core(mpan_cores[i])
    else:
        mpan_cores = None

    running_name, finished_name = chellow.dloads.make_names(
        "supplies_snapshot.csv", user
    )

    args = (running_name, finished_name, date, supply_id, mpan_cores)
    threading.Thread(target=content, args=args).start()
    return chellow_redirect("/downloads", 303)
