"""Controller -- owns the obdcore link/registry/store/scheduler for the GUI.

Keeps all acquisition concerns out of the widgets.  The GUI subscribes/
unsubscribes PIDs (== what's polled == what's plotted) and reads the store on
a timer; the scheduler thread does the serial work.
"""
import time

from obdcore import (PidRegistry, DtcDatabase, TimeSeriesStore, PollScheduler,
                     CsvRecorder, load_default, load_profile)
from obdcore.mock import MockLink
from obdcore.trip import TripComputer, PerformanceMeter

# default poll rates (Hz) -- fast for the no-start metrics, slower for the rest
FAST = {"ICP", "FICM_M", "RPM"}
DEFAULT_HZ = 2
FAST_HZ = 5

# DTC services: result-bucket label, request mode string, response service byte
# (mode "03"->0x43 stored, "07"->0x47 pending, "0A"->0x4A permanent)
DTC_SERVICES = (
    ("stored", "03", 0x43),
    ("pending", "07", 0x47),
    ("permanent", "0A", 0x4A),
)


class Controller:
    def __init__(self):
        self.profile = load_default()
        self.reg = PidRegistry(self.profile)
        self.dtcdb = DtcDatabase(self.profile)
        self.store = TimeSeriesStore()
        self.link = None
        self.sched = None
        self.t0 = None
        self.connected = False
        self.trip = TripComputer()
        self.perf = PerformanceMeter()
        self.speed_key = None        # PID key for standard speed (mode 01 0D)
        self.maf_key = None          # PID key for standard MAF  (mode 01 10)

    def _find_std_keys(self):
        """Locate the speed/MAF PIDs (mode 01, pid 0D/10) by any key name."""
        self.speed_key = self.maf_key = None
        for p in self.reg.all():
            if p.mode == "01" and p.pid.upper() == "0D":
                self.speed_key = p.key
            elif p.mode == "01" and p.pid.upper() == "10":
                self.maf_key = p.key

    def load_profile(self, path):
        """Switch the active vehicle profile (only allowed while disconnected)."""
        self.profile = load_profile(path)
        self.reg = PidRegistry(self.profile)
        self.dtcdb = DtcDatabase(self.profile)

    def connect(self, port=None, baud=38400, mock=False, conn=None):
        """conn: optional {'kind': 'serial'|'wifi'|'ble', ...}. Falls back to
        serial(port, baud) for backward compatibility."""
        if mock:
            self.link = MockLink(clock=time.time)
        else:
            from obdcore.link import ElmLink            # imported lazily (needs pyserial)
            c = conn or {"kind": "serial", "port": port, "baud": baud}
            kind = c.get("kind", "serial")
            if kind == "wifi":
                self.link = ElmLink.tcp(c["host"], c.get("port", 35000))
            elif kind == "ble":
                self.link = ElmLink.ble(c["address"])
            else:
                self.link = ElmLink.serial(c.get("port", port), c.get("baud", baud))
            self.link.init()
        ok = self.link.connect()
        try:
            self.link.fast_timing(True)
        except Exception:
            pass
        self.sched = PollScheduler(self.link, self.reg, self.store, clock=time.time)
        self.t0 = time.time()
        self.connected = True
        self.trip.reset()
        self.perf = PerformanceMeter()
        # keep speed + MAF polled in the background so trip/performance always run
        self._find_std_keys()
        if self.speed_key:
            self.sched.subscribe(self.speed_key, 2)
        if self.maf_key:
            self.sched.subscribe(self.maf_key, 2)
        return ok

    def hz_for(self, key):
        return FAST_HZ if key in FAST else DEFAULT_HZ

    def subscribe(self, key):
        if self.sched:
            self.sched.subscribe(key, self.hz_for(key))

    def unsubscribe(self, key):
        if self.sched:
            self.sched.unsubscribe(key)

    def subscribed(self):
        return set(self.sched.subscriptions()) if self.sched else set()

    def start(self):
        if self.sched:
            self.sched.start()

    def record(self, path):
        self.store.recorder = CsvRecorder(path)

    def stop_record(self):
        if self.store.recorder:
            self.store.recorder.close()
            self.store.recorder = None

    def now(self):
        return (time.time() - self.t0) if self.t0 else 0.0

    # -- diagnostics (DTCs) --------------------------------------------------
    # All link access goes through the scheduler's one-off path when a
    # scheduler exists, so a DTC read/clear never races the polling thread for
    # the serial link.  When disconnected (no scheduler), call the link direct.
    def _oneoff(self, fn, timeout=8.0):
        if self.sched is not None:
            return self.sched.run_oneoff(fn, timeout=timeout)
        if self.link is not None:
            return fn()
        raise RuntimeError("not connected")

    def read_dtcs(self):
        """Read stored (03), pending (07) and permanent (0A) DTCs.
        Returns {"stored": [...], "pending": [...], "permanent": [...]}."""
        out = {}
        for label, mode, svc in DTC_SERVICES:
            out[label] = self._oneoff(
                lambda m=mode, s=svc: self.link.read_dtcs(m, s)) or []
        return out

    def clear_dtcs(self):
        """Mode 04: clear stored+pending codes and freeze frame.
        Returns True if the ECU acknowledged."""
        return bool(self._oneoff(lambda: self.link.clear_dtcs()))

    # -- standard OBD services (via the one-off path) --
    def read_vehicle_info(self):
        return self._oneoff(lambda: self.link.read_vehicle_info())

    def read_readiness(self):
        return self._oneoff(lambda: self.link.read_readiness())

    def read_freeze_frame(self):
        return self._oneoff(lambda: self.link.read_freeze_frame())

    # -- bi-directional / service actions --
    def actions(self):
        return self.profile.actions or []

    def run_action(self, action):
        from obdcore.actions import run_action
        return self._oneoff(lambda: run_action(action, self.link), timeout=20.0)

    # -- trip / performance (fed from the live store each GUI tick) --
    def update_trip(self):
        spd = self.store.latest(self.speed_key) if self.speed_key else None
        maf = self.store.latest(self.maf_key) if self.maf_key else None
        now = time.time()
        self.trip.update(now, spd, maf)
        self.perf.update(now, spd)
        return spd, maf

    def stop(self):
        if self.sched:
            self.sched.stop()
            self.sched = None
        self.stop_record()
        if self.link:
            try:
                self.link.fast_timing(False)
            except Exception:
                pass
            self.link.close()
            self.link = None
        self.connected = False
