"""Deye inverter writer and live register reader."""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any

import asyncpg

from services.control.deye_helpers import (
    BATT_VOLTAGE_V,
    DEYE_CLOCK_DRIFT_OK_SEC,
    DEYE_CLOCK_REGS,
    DEYE_CLOCK_RESYNC_INTERVAL_HOURS,
    DEYE_TOU_INACTIVE_HHMM,
    PRAGUE_TZ,
    REG178_MI_EXPORT_DISABLE,
    REG178_MI_EXPORT_ENABLE,
    REG178_MI_EXPORT_MASK,
    REG178_PASSIVE,
    REG178_SELL,
    REG178_VERIFY_MASK,
    REG178_VERIFY_MASK_COMBINED,
    _DEYE_INACTIVE_TOU_REGISTERS,
    _deye_should_skip_time_sync_after_read,
    deye_mi_export_cutoff_want_enabled,
    _prague_minute_start_utc,
    current_slot_hhmm,
    next_slot_hhmm,
)
from services.control.modbus_journal import (
    _drop_registers_matching_last_verified,
    _fetch_last_verified_inverter_registers,
    create_modbus_commands,
    execute_modbus_commands,
)
from services.control.models import ControlSetpoints
from services.control.repository import _get_current_soc, _load_inverter_config
from services.control.setpoints import (
    _deye_reg143_export_w,
    _deye_system_time_register_rows,
    _deye_time_point_rows,
    _deye_tou_min_soc_pct,
    _deye_tou_params,
    _deye_tou_reserve_soc_pct,
    deye_battery_charge_discharge_amps,
    get_deye_mode,
)
from services.modbus_client import get_modbus_client

logger = logging.getLogger(__name__)


async def write_inverter_setpoints(
    site_id: int,
    setpoints_now: ControlSetpoints,
    setpoints_next: ControlSetpoints | None,
    db: asyncpg.Connection,
    planning_run_id: int | None = None,
) -> str:
    inv = await _load_inverter_config(site_id, db)
    if inv is None:
        return "FAIL inverter: no controllable Modbus endpoint"

    unit_id = int(inv.unit_id if inv.unit_id is not None else 1)
    raw_bat = setpoints_now.battery_w
    grid_w = int(setpoints_now.grid_setpoint_w or 0)
    no_export = inv.no_export
    export_lim_hw = _deye_reg143_export_w(no_export, inv.max_export_power_w)
    export_lim = export_lim_hw
    if int(setpoints_now.grid_export_limit or 0) > 0:
        export_lim = min(export_lim_hw, int(setpoints_now.grid_export_limit))
    max_batt_w_discharge = int(inv.max_discharge_a * BATT_VOLTAGE_V)
    tp_discharge_w = 0 if setpoints_now.lock_battery else max_batt_w_discharge
    tou_min_pct = _deye_tou_min_soc_pct(inv)
    tou_reserve_pct = _deye_tou_reserve_soc_pct(inv)

    try:
        soc_telemetry = await _get_current_soc(site_id, db)
        deye_mode = get_deye_mode(setpoints_now)

        bat_w = int(raw_bat) if raw_bat is not None else 0
        charge_a, discharge_a = deye_battery_charge_discharge_amps(
            lock_battery=setpoints_now.lock_battery,
            deye_mode=deye_mode,
            self_sustain_local_use=setpoints_now.self_sustain_local_use,
            bat_w=bat_w,
            grid_w=grid_w,
            max_charge_a=int(inv.max_charge_a),
            max_discharge_a=int(inv.max_discharge_a),
            export_mode=setpoints_now.export_mode,
            export_ban=bool(setpoints_now.export_ban),
        )

        zero_exp_mode = int(inv.deye_zero_export_mode or 1)
        selling_mode = 0 if deye_mode == "SELL" else zero_exp_mode
        solar_sell = 0 if (setpoints_now.export_ban and deye_mode != "SELL") else 1
        export_limit = export_lim
        reg178_val = REG178_SELL if deye_mode == "SELL" else REG178_PASSIVE

        charge_a_log = charge_a if charge_a is not None else "skip"
        logger.info(
            f"[control] site={site_id} fyzický režim Deye: {deye_mode} | "
            f"battery_w={raw_bat!r} grid_w={grid_w} | "
            f"charge_a={charge_a_log} discharge_a={discharge_a} | "
            f"reg142={selling_mode} reg145={solar_sell} reg143={export_limit}W reg178={reg178_val}"
        )

        now_cet, time_rows = _deye_system_time_register_rows()
        skip_time = False
        try:
            mb_clock = await get_modbus_client(inv.host, inv.port)
            tvals = await mb_clock.read_holding_registers(
                62, 3, int(inv.unit_id if inv.unit_id is not None else 1)
            )
            if len(tvals) == 3:
                skip_time = _deye_should_skip_time_sync_after_read(
                    inv, int(tvals[0]), int(tvals[1]), int(tvals[2])
                )
            else:
                logger.warning(
                    "Deye clock read: expected 3 registers, got %s; will sync 62-64",
                    len(tvals),
                )
        except Exception as e:
            logger.warning("Deye clock read failed (will sync 62-64): %s", e)

        if skip_time:
            logger.info(
                "Deye clock 62-64 skipped (drift <= %ss, last sync < %sh ago): %s CET",
                DEYE_CLOCK_DRIFT_OK_SEC,
                DEYE_CLOCK_RESYNC_INTERVAL_HOURS,
                now_cet.strftime("%Y-%m-%d %H:%M:%S"),
            )
        else:
            logger.info("Deye time will sync: %s CET", now_cet.strftime("%Y-%m-%d %H:%M:%S"))

        registers: list[tuple[int, str, int]] = [] if skip_time else list(time_rows)

        sp_tp2 = setpoints_next if setpoints_next is not None else setpoints_now
        hh_cur = current_slot_hhmm()
        hh_nxt = next_slot_hhmm()
        p1, s1, g1 = _deye_tou_params(setpoints_now, inv)
        p2, s2, g2 = _deye_tou_params(sp_tp2, inv)
        registers.extend(_deye_time_point_rows(0, hh_cur, p1, s1, g1))
        registers.extend(_deye_time_point_rows(1, hh_nxt, p2, s2, g2))

        prague_date = datetime.now(PRAGUE_TZ).date()
        inactive_sig = (
            f"{DEYE_TOU_INACTIVE_HHMM}|{tou_min_pct}|{tou_reserve_pct}|{tp_discharge_w}"
        )
        need_inactive_tou = (
            inv.deye_last_tou_inactive_write_prague_date != prague_date
            or inv.deye_tou_inactive_signature != inactive_sig
        )
        if need_inactive_tou:
            for idx in range(2, 6):
                registers.extend(
                    _deye_time_point_rows(
                        idx, DEYE_TOU_INACTIVE_HHMM, tp_discharge_w, tou_min_pct, False
                    )
                )
        else:
            logger.debug(
                "Deye TOU rows 3-6 skipped (already written today, signature unchanged)"
            )

        amp_regs: list[tuple[int, str, int]] = []
        if charge_a is not None:
            amp_regs.append((108, "", charge_a))
        amp_regs.append((109, "", discharge_a))
        registers.extend(
            amp_regs
            + [
                (141, "energy_mode (0)", 0),
                (142, "limit_control", selling_mode),
                (143, "", export_limit),
                (145, "solar_sell", solar_sell),
            ]
        )

        if (
            bool(inv.deye_reg340_pv_a_control_enabled)
            and int(inv.pv_a_cap_w) > 0
            and setpoints_now.pv_a_allowed_w is not None
        ):
            registers.append((340, "max_solar_power_w", int(setpoints_now.pv_a_allowed_w)))

        try:
            mb178 = await get_modbus_client(inv.host, inv.port)
            r178 = await mb178.read_holding_registers(178, 1, unit_id)
            if not r178 or len(r178) < 1:
                raise OSError(f"reg178 read returned {len(r178) if r178 is not None else None} values")
            current_178 = int(r178[0])
            peak_bits = int(reg178_val) & int(REG178_VERIFY_MASK)
            if inv.deye_gen_microinverter_cutoff_enabled:
                want_cutoff = deye_mi_export_cutoff_want_enabled(
                    gen_microinverter_cutoff_enabled=True,
                    deye_gen_cutoff_enabled=bool(setpoints_now.deye_gen_cutoff_enabled),
                    export_ban=bool(setpoints_now.export_ban),
                    deye_mode=deye_mode,
                )
                mi_bits = REG178_MI_EXPORT_ENABLE if want_cutoff else REG178_MI_EXPORT_DISABLE
            else:
                mi_bits = int(current_178) & int(REG178_MI_EXPORT_MASK)

            new_178 = (
                (int(current_178) & ~int(REG178_VERIFY_MASK_COMBINED))
                | int(peak_bits)
                | int(mi_bits)
            )
            registers.append((178, "control_board_special_1", int(new_178)))
            logger.info(
                "[control] %s: reg178 (control_board_special_1) old=%s new=%s peak_bits=0x%04X mi_bits=%s",
                inv.code,
                current_178,
                new_178,
                int(peak_bits),
                int(mi_bits),
            )
        except Exception as e:
            logger.warning("[control] %s: reg178 RMW failed (skip reg178 write): %s", inv.code, e)

        logger.info(
            "[control] %s: deye_mode=%s charge=%sA discharge=%sA "
            "reg142=%s reg145=%s export=%sW "
            "tp1=%s tp2=%s soc=%s%% (batt=%r grid=%sW)",
            inv.code,
            deye_mode,
            charge_a,
            discharge_a,
            selling_mode,
            solar_sell,
            export_limit,
            hh_cur,
            hh_nxt,
            soc_telemetry,
            raw_bat,
            grid_w,
        )

        last_verified = await _fetch_last_verified_inverter_registers(site_id, inv.id, db)
        registers, skipped_unchanged = _drop_registers_matching_last_verified(
            registers, last_verified
        )
        if skipped_unchanged:
            logger.info(
                "[control] %s: skip %s registers (value equals last verified): %s",
                inv.code,
                len(skipped_unchanged),
                skipped_unchanged[:24],
            )
        if not registers:
            logger.info(
                "[control] %s: all Deye holding regs match last verified, no Modbus write",
                inv.code,
            )
            if need_inactive_tou:
                await db.execute(
                    """
                    UPDATE ems.asset_inverter
                    SET deye_last_tou_inactive_write_prague_date = $1,
                        deye_tou_inactive_signature = $2
                    WHERE id = $3
                    """,
                    prague_date,
                    inactive_sig,
                    inv.id,
                )
            return (
                f"OK inverter: batt_w={raw_bat!r} (no changes vs last verified Modbus snapshot)"
            )

        will_write_inactive = any(
            int(r) in _DEYE_INACTIVE_TOU_REGISTERS for r, _, _ in registers
        )

        cmd_ids = await create_modbus_commands(
            site_id,
            planning_run_id,
            "inverter",
            inv.id,
            inv.code,
            inv.host,
            inv.port,
            inv.unit_id,
            registers,
            db,
            deye_physical_mode=deye_mode,
        )
        if not await execute_modbus_commands(cmd_ids, db):
            return f"FAIL inverter: {inv.code}: Modbus write failed (see modbus_command)"
        logger.info("[control] Inverter %s journal write OK", inv.code)

        will_write_time = any(int(r) in DEYE_CLOCK_REGS for r, _, _ in registers)
        if will_write_time:
            await db.execute(
                """
                UPDATE ems.asset_inverter
                SET deye_last_system_time_sync_minute = $1,
                    deye_last_system_time_sync_at = now()
                WHERE id = $2
                """,
                _prague_minute_start_utc(),
                inv.id,
            )

        if need_inactive_tou or will_write_inactive:
            await db.execute(
                """
                UPDATE ems.asset_inverter
                SET deye_last_tou_inactive_write_prague_date = $1,
                    deye_tou_inactive_signature = $2
                WHERE id = $3
                """,
                prague_date,
                inactive_sig,
                inv.id,
            )
    except Exception as e:
        return f"FAIL inverter: {inv.code}: {e}"

    return (
        f"OK inverter: batt_w={raw_bat!r} "
        f"(time points + FC 0x10: 108/109/141/142/178/143/145/340 dle plánu)"
    )


async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict[str, Any]:
    """
    Živé čtení holding registrů Deye 108, 109, 141-145, 178, 191 a volitelně 340.
    """
    inv = await _load_inverter_config(site_id, db)
    if inv is None:
        raise ValueError("no controllable Modbus inverter for site")

    uid = int(inv.unit_id)
    client = await get_modbus_client(inv.host, inv.port)
    read_at = datetime.now(timezone.utc)
    try:
        async with client.batch(uid) as mb:
            b108 = await mb.read_holding_registers(108, 2)
            b141 = await mb.read_holding_registers(141, 5)
            r178 = await mb.read_holding_registers(178, 1)
            r191 = await mb.read_holding_registers(191, 1)
            if inv.deye_reg340_pv_a_control_enabled:
                r340 = await mb.read_holding_registers(340, 1)
            else:
                r340 = None
        r108, r109 = b108[0], b108[1]
        r141, r142, r143 = b141[0], b141[1], b141[2]
        r145 = b141[4]
        r178 = r178[0]
        r191 = r191[0]
        r340v = int(r340[0]) if r340 is not None and len(r340) >= 1 else None
    except Exception:
        logger.exception("read_deye_registers_live site=%s failed", site_id)
        raise

    return {
        "reg108_charge_a": int(r108),
        "reg109_discharge_a": int(r109),
        "reg141_energy_mode": int(r141),
        "reg142_limit_control": int(r142),
        "reg143_export_limit_w": int(r143),
        "reg145_solar_sell": int(r145),
        "reg178_peak_shaving_switch": int(r178),
        "reg178_control_board_special_1": int(r178),
        "reg178_mi_export_cutoff_bits": int(r178) & int(REG178_MI_EXPORT_MASK),
        "reg178_mi_export_cutoff_is_on": (int(r178) & int(REG178_MI_EXPORT_MASK))
        == int(REG178_MI_EXPORT_ENABLE),
        "reg191_peak_shaving_w": int(r191),
        "reg340_max_solar_power_w": r340v,
        "read_at": read_at.isoformat(),
    }
