#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time
import csv
import os
import sys
import struct
import requests
from datetime import datetime
from typing import List, Any, Callable, Optional

try:
    from zoneinfo import ZoneInfo  # Python 3.9+
    TZ = ZoneInfo("Europe/Prague")
except Exception:
    TZ = None  # fallback na lokální čas

# ================== KONFIGURACE ==================
API_URL = "http://localhost:8443/modbus/call"
API_KEY = "tajny-klic"

# Interval opakování v sekundách
INTERVAL_SECONDS = 100.0

# Kořen projektu = adresář, kde leží tento skript
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_DIR = os.path.join(BASE_DIR, "logs")

# ---- První čtení (raw -> float) ----
FIRST_INSTR = 1
FIRST_ADDR = 0
FLOAT_COUNT_NEEDED = 12           # dle vzoru: 8 čísel po čase
WORD_SWAP = False                # True, pokud je prohozené pořadí slov 16b registrů

# ---- Druhé čtení (raw -> bool -> 0/1) ----
SECOND_INSTR = 2
SECOND_ADDR = 16384
DI_COUNT_NEEDED = 12             # dle vzoru: 12 bitů (0/1)

# SSL: lokální self-signed cert
VERIFY_SSL = False

# Tolerance pro porovnání mezi čteními: ±33 % (0.33)
REL_TOL = 0.33
ABS_FLOOR = 2.0                  # minimální absolutní tolerance (kvůli malým číslům)

# Rozsahy pro „float“ sloupce – indexováno 1.. atd. (po čase)
# 1–6 : 0–120 ; 7–12 : 200–250 (podle zadání)
RANGES = [(0.0, 120.0)] * 6 + [(200.0, 250.0)] * 6
# ==================================================

# Potlačíme warningy pro self-signed cert, když VERIFY_SSL=False
requests.packages.urllib3.disable_warnings(
    category=requests.packages.urllib3.exceptions.InsecureRequestWarning
)

def now_local() -> datetime:
    if TZ is not None:
        return datetime.now(TZ)
    return datetime.now()

def current_csv_path(dt: datetime) -> str:
    """logs/DD_MM_RR.csv"""
    filename = dt.strftime("%d_%m_%y.csv")
    return os.path.join(LOG_DIR, filename)

def ensure_dir(path: str):
    os.makedirs(path, exist_ok=True)

def call_modbus(payload: dict) -> List[Any]:
    headers = {
        "Content-Type": "application/json",
        "X-API-Key": API_KEY,
    }
    r = requests.post(API_URL, json=payload, headers=headers, verify=VERIFY_SSL, timeout=10)
    r.raise_for_status()
    data = r.json()

    for key in ("data", "result", "values", "response"):
        if isinstance(data, dict) and key in data:
            vals = data[key]
            if isinstance(vals, dict):
                for k2 in ("data", "values"):
                    if k2 in vals:
                        return vals[k2]
            return vals
    if isinstance(data, list):
        return data
    raise ValueError(f"Neočekávaný formát odpovědi: {data}")

def regs_to_floats(registers: List[int], word_swap: bool = False) -> List[float]:
    """16bit registry -> list float32 (IEEE-754). Každé 2 registry = 1 float."""
    if len(registers) < 2:
        return []
    if len(registers) % 2 != 0:
        registers = registers[:-1]

    floats = []
    for i in range(0, len(registers), 2):
        w1, w2 = registers[i], registers[i + 1]
        if word_swap:
            w1, w2 = w2, w1
        b = struct.pack(">H", int(w1) & 0xFFFF) + struct.pack(">H", int(w2) & 0xFFFF)
        f = struct.unpack(">f", b)[0]
        floats.append(f)
    return floats

def round2(values: List[float]) -> List[float]:
    out = []
    for v in values:
        try:
            out.append(round(float(v), 2))
        except Exception:
            out.append(0.0)
    return out

def clamp(v: float, lo: float, hi: float) -> float:
    if v < lo: return lo
    if v > hi: return hi
    return v

def within(v: float, ref: float, rel: float, abs_floor: float) -> bool:
    # pro malé ref použijeme minimální absolutní toleranci
    tol = max(abs(ref) * rel, abs_floor)
    return abs(v - ref) <= tol

def pad_or_trim(lst: List[Any], n: int, pad_val: Any = 0) -> List[Any]:
    if len(lst) >= n:
        return lst[:n]
    return lst + [pad_val] * (n - len(lst))

def read_floats_once(ip: str, port: int, instr: int, addr: int,
                     float_count: int, word_swap: bool) -> List[float]:
    # 2 registry na 1 float
    count_regs = max(2 * float_count, 2)  # aspoň 2
    payload = {
        "ip": ip,
        "port": port,
        "instr": instr,
        "addr": addr,
        "count": count_regs,
        "type": "raw",
    }
    regs = call_modbus(payload)
    vals = regs_to_floats(regs, word_swap=word_swap)
    vals = round2(vals)
    vals = pad_or_trim(vals, float_count, 0.0)
    return vals

def read_discrete_once(ip: str, port: int, instr: int, addr: int,
                       di_count: int) -> List[int]:
    payload = {
        "ip": ip,
        "port": port,
        "instr": instr,
        "addr": addr,
        "count": di_count,
        "type": "raw",
    }
    raw = call_modbus(payload)
    out = []
    for v in raw:
        if isinstance(v, bool):
            out.append(1 if v else 0)
        else:
            out.append(1 if v else 0)
    return pad_or_trim(out, di_count, 0)

def triple_read_with_validation(reader: Callable[[], List[float]]) -> List[float]:
    """
    1. načtení A
    2. načtení B
       - pokud B ∈ (A ± 33 %) → výsledek = B
       - jinak 3. načtení C:
           - pokud C ∈ (A ± 33 %) → výsledek = A
           - elif C ∈ (B ± 33 %) → výsledek = B
           - else → výsledek = B (a následně provedeme clamp do očekávaných rozsahů)
    Porovnání probíhá po prvcích.
    """
    A = reader()
    B = reader()

    # pro každý index posoudíme samostatně
    result: List[float] = []
    C: Optional[List[float]] = None

    need_third = any(
        not within(B[i], A[i], REL_TOL, ABS_FLOOR)
        for i in range(min(len(A), len(B)))
    )

    if need_third:
        C = reader()

    n = min(len(A), len(B)) if C is None else min(len(A), len(B), len(C))
    for i in range(n):
        a, b = A[i], B[i]
        if C is not None:
            c = C[i]
            if within(b, a, REL_TOL, ABS_FLOOR):
                chosen = b
            else:
                if within(c, a, REL_TOL, ABS_FLOOR):
                    chosen = a
                elif within(c, b, REL_TOL, ABS_FLOOR):
                    chosen = b
                else:
                    chosen = b  # fallback
        else:
            # B už je „dost blízko“ A, vezmeme B
            chosen = b
        result.append(chosen)

    # Pokud by A/B/C měly různé délky (neměly by), dorovnáme
    result = pad_or_trim(result, len(A), 0.0)
    return result

def apply_ranges(vals: List[float]) -> List[float]:
    """
    Aplikuje rozsahy (clamp) podle zadání:
    - sloupce 1–6: 0–120
    - sloupce 7–12: 200–250
    Pokud máme méně než 12 hodnot, platí dostupná část.
    """
    out = []
    for i, v in enumerate(vals, start=1):
        if i <= len(RANGES):
            lo, hi = RANGES[i - 1]
            out.append(clamp(v, lo, hi))
        else:
            out.append(v)
    return out

def rotate_if_needed(current_path: str) -> str:
    new_path = current_csv_path(now_local())
    if new_path != current_path:
        return new_path
    return current_path

def main():
    ensure_dir(LOG_DIR)

    ip = "192.168.11.155"
    port = 503

    # Předvýpočet cesty a otevření na první zápis (bez hlavičky)
    current_path = current_csv_path(now_local())
    print(f"[OK] Loguji do: {current_path} (interval {INTERVAL_SECONDS}s). Ukonči Ctrl+C.")

    def floats_reader():
        return read_floats_once(
            ip=ip, port=port, instr=FIRST_INSTR, addr=FIRST_ADDR,
            float_count=FLOAT_COUNT_NEEDED, word_swap=WORD_SWAP
        )

    try:
        while True:
            # případná denní rotace
            current_path = rotate_if_needed(current_path)

            # 3-krokové načtení + validace + range clamp
            try:
                floats = triple_read_with_validation(floats_reader)
                floats = apply_ranges(floats)
                floats = round2(floats)  # finální kosmetické zarovnání na 2 DP
            except Exception as e:
                print(f"[ERR] Čtení (float): {e}", file=sys.stderr)
                floats = [0.0] * FLOAT_COUNT_NEEDED

            # DI jednorázově
            try:
                di = read_discrete_once(
                    ip=ip, port=port, instr=SECOND_INSTR, addr=SECOND_ADDR,
                    di_count=DI_COUNT_NEEDED
                )
            except Exception as e:
                print(f"[ERR] Čtení (DI): {e}", file=sys.stderr)
                di = [0] * DI_COUNT_NEEDED

            # příprava řádku: HH:MM, 8 floatů, 12 bitů
            dt = now_local()
            hhmm = dt.strftime("%H:%M")
            # CSV: bez hlavičky, oddělovač „,“
            row = [hhmm] + floats[:FLOAT_COUNT_NEEDED] + di[:DI_COUNT_NEEDED]

            try:
                with open(current_path, "a", newline="", encoding="utf-8") as f:
                    w = csv.writer(f, delimiter=",")
                    w.writerow(row)
            except Exception as e:
                print(f"[ERR] Zápis do CSV: {e}", file=sys.stderr)

            time.sleep(INTERVAL_SECONDS)

    except KeyboardInterrupt:
        print("\n[STOP] Ukončeno uživatelem.")

if __name__ == "__main__":
    main()

