"""Simulated working tank + TDS/pH dosing controller (runs without hardware)."""
import random
import statistics
import time
from dataclasses import dataclass, field


# ---------------------------------------------------------------- plant model
@dataclass
class Tank:
    volume_l: float = 100.0
    tds_ppm: float = 800.0
    ph: float = 6.6
    ppm_per_ml: float = 2.0      # TDS rise per mL of A+B+C mix in this tank
    ph_per_ml_acid: float = 0.02 # pH drop per mL of pH-down
    noise: float = 0.0

    def add_fertilizer(self, ml):
        self.tds_ppm += ml * self.ppm_per_ml

    def add_acid(self, ml):
        self.ph -= ml * self.ph_per_ml_acid

    def add_alkali(self, ml):
        self.ph += ml * self.ph_per_ml_acid

    def read(self):
        n = self.noise
        return (self.tds_ppm + random.gauss(0, n * 10),
                self.ph + random.gauss(0, n * 0.01))


# ---------------------------------------------------------------- controller
@dataclass
class Pump:
    name: str
    flow_ml_s: float             # from pump calibration

    def seconds_for(self, ml):
        return ml / self.flow_ml_s


@dataclass
class TDSController:
    target: float
    tolerance: float = 5.0
    gain: float = 0.8
    ppm_per_ml: float = 2.0      # learned or entered by the grower
    ratio: dict = field(default_factory=lambda: {"A": 2, "B": 2, "C": 1})
    error_min_change: float = 3.0  # ppm a dose must move the reading
    max_dry_doses: int = 3
    dry_doses: int = 0
    blocked: bool = False

    def step(self, reading, last_reading=None, last_dose_ml=0.0):
        """Return {component: mL} to dose now (empty dict = nothing to do)."""
        if self.blocked:
            return {}
        # controller-error check: we dosed, but the sensor did not move
        if last_dose_ml > 0 and last_reading is not None:
            if abs(reading - last_reading) < self.error_min_change:
                self.dry_doses += 1
                if self.dry_doses >= self.max_dry_doses:
                    self.blocked = True       # needs manual restart
                    return {}
            else:
                self.dry_doses = 0
        error = self.target - reading
        if error <= self.tolerance:         # fertilizer can only raise TDS
            return {}
        total_ml = self.gain * error / self.ppm_per_ml
        parts = sum(self.ratio.values())
        return {k: total_ml * v / parts for k, v in self.ratio.items()}


def is_stable(readings, threshold):
    return len(readings) >= 3 and statistics.pstdev(readings) < threshold


# ---------------------------------------------------------------- demo
if __name__ == "__main__":
    random.seed(1)
    tank = Tank()
    pumps = {k: Pump(k, 1.2) for k in "ABC"}
    ctl = TDSController(target=1000)

    last_reading, last_dose = None, 0.0
    for cycle in range(1, 8):
        tds, _ = tank.read()
        dose = ctl.step(tds, last_reading, last_dose)
        if not dose:
            print(f"cycle {cycle}: TDS {tds:7.1f} ppm  -> in tolerance, stop")
            break
        total = sum(dose.values())
        times = ", ".join(f"{k} {pumps[k].seconds_for(v):4.1f}s"
                          for k, v in dose.items())
        print(f"cycle {cycle}: TDS {tds:7.1f} ppm  -> dose {total:5.1f} mL ({times})")
        tank.add_fertilizer(total)
        last_reading, last_dose = tds, total
        # (on hardware: run pumps, then wait the pause regulator, e.g. 120 s)