import json
import random
import threading
import time
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from urllib.parse import parse_qs, urlparse


SENSOR_COUNT = 16
VALVE_COUNT = 32
DEFAULT_OPEN_DEGREES_MAX = 90


def make_default_sensors():
    return [
        {
            "id": f"zone_{number}",
            "name": f"Датчик {number}",
            "value": 24.0 + (number % 4),
            "setpoint": 28.0,
            "unit": "°C",
            "zone": str(number),
            "ds18b20Id": f"28-00-00-00-00-00-00-{number:02X}",
        }
        for number in range(1, SENSOR_COUNT + 1)
    ]


def make_default_valves():
    valves = []
    for index in range(VALVE_COUNT):
        number = index + 1
        zone = (index % SENSOR_COUNT) + 1
        valves.append(
            {
                "id": f"valve_{number}",
                "name": f"Клапан {number}",
                "zone": str(zone),
                "mode": "auto",
                "position": 0,
                "targetTemp": 28,
                "isOpen": False,
                "openDegrees": 0,
                "openDegreesMax": DEFAULT_OPEN_DEGREES_MAX,
            }
        )
    return valves


SENSORS = make_default_sensors()
VALVES = make_default_valves()
SERIAL_STATE = {"connected": False, "transport": "rtu", "port": None, "unitId": 3}

state_lock = threading.Lock()


def clamp(value, min_value, max_value):
    return max(min_value, min(max_value, value))


def open_degrees_max(valve):
    return max(1, int(valve.get("openDegreesMax", DEFAULT_OPEN_DEGREES_MAX)))


def position_to_degrees(position, valve):
    max_degrees = open_degrees_max(valve)
    return clamp(round(position * max_degrees / 100), 0, max_degrees)


def apply_open_counter(valve):
    valve["openDegrees"] = position_to_degrees(valve.get("position", 0), valve)


def sensor_by_zone(zone):
    for sensor in SENSORS:
        if str(sensor["zone"]) == str(zone):
            return sensor
    return None


def valve_by_id(valve_id):
    for valve in VALVES:
        if valve["id"] == valve_id:
            return valve
    return None


def calibrate_valve(valve):
    valve["position"] = 0
    valve["openDegrees"] = 0
    valve["isOpen"] = False


def calibrate_all_valves():
    for valve in VALVES:
        calibrate_valve(valve)


def update_physics_loop():
    while True:
        with state_lock:
            for sensor in SENSORS:
                valve = valve_by_id(f"valve_{sensor['zone']}")
                if not valve:
                    continue
                if valve["mode"] == "manual":
                    target = 20 + clamp(valve["position"], 0, 100) * 0.7
                else:
                    target = float(valve.get("targetTemp", sensor["setpoint"]))

                drift = (target - sensor["value"]) * 0.07
                noise = (random.random() - 0.5) * 0.2
                sensor["value"] = clamp(sensor["value"] + drift + noise, -40, 150)
                sensor["value"] = round(sensor["value"], 2)

            for valve in VALVES:
                apply_open_counter(valve)
                valve["isOpen"] = valve.get("position", 0) > 0
        time.sleep(2.0)


class Handler(SimpleHTTPRequestHandler):

    def end_headers(self):
        self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
        self.send_header("Pragma", "no-cache")
        self.send_header("Expires", "0")
        super().end_headers()
    def _send_json(self, payload, code=200):
        body = json.dumps(payload).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()

    def do_POST(self):
        parsed = urlparse(self.path)
        parts = [seg for seg in parsed.path.split("/") if seg]

        if parsed.path in ("/api/serial/connect", "/api/connect"):
            length = int(self.headers.get("Content-Length", "0") or "0")
            body = self.rfile.read(length)
            try:
                payload = json.loads(body.decode("utf-8", errors="ignore")) if body else {}
            except Exception:
                payload = {}
            transport = str(payload.get("transport", payload.get("mode", "rtu"))).lower()
            try:
                unit_id = int(payload.get("unitId", payload.get("slaveId", payload.get("slave", SERIAL_STATE.get("unitId", 3)))))
            except Exception:
                unit_id = 3
            unit_id = max(1, min(247, unit_id))
            if transport in ("tcp", "modbus_tcp", "modbus-tcp"):
                host = str(payload.get("host") or payload.get("ip") or "127.0.0.1").strip()
                tcp_port = int(payload.get("tcpPort", payload.get("tcp_port", payload.get("port", 502))))
                SERIAL_STATE.update({
                    "connected": True,
                    "transport": "tcp",
                    "unitId": unit_id,
                    "host": host,
                    "tcpPort": tcp_port,
                    "address": f"{host}:{tcp_port}",
                    "port": f"{host}:{tcp_port}",
                })
            else:
                port = str(payload.get("port") or "COM1").strip()
                SERIAL_STATE.update({"connected": True, "transport": "rtu", "port": port, "unitId": unit_id})
            self._send_json({"ok": True, **SERIAL_STATE})
            return

        if parsed.path in ("/api/serial/disconnect", "/api/disconnect"):
            transport = SERIAL_STATE.get("transport", "rtu")
            SERIAL_STATE.update({"connected": False, "transport": transport, "port": None})
            self._send_json({"ok": True, **SERIAL_STATE})
            return

        if len(parts) == 3 and parts[0] == "api" and parts[1] == "valves" and parts[2] == "calibrate-all":
            with state_lock:
                calibrate_all_valves()
                self._send_json({"ok": True, "valves": [dict(item) for item in VALVES], "message": "calibration started"})
            return

        if len(parts) == 4 and parts[0] == "api" and parts[1] == "valves" and parts[3] == "calibrate":
            valve_id = parts[2]
            with state_lock:
                valve = valve_by_id(valve_id)
                if not valve:
                    self.send_response(404)
                    self.end_headers()
                    return
                calibrate_valve(valve)
                self._send_json({"ok": True, "valve": dict(valve), "message": "calibration started"})
            return

        self.send_response(404)
        self.end_headers()

    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path in ("/api/serial/status", "/api/state"):
            self._send_json({"ok": True, **SERIAL_STATE})
            return
        if parsed.path in ("/api/serial/ports", "/api/ports"):
            self._send_json({"ok": True, "ports": [], "selected": SERIAL_STATE.get("port")})
            return
        if parsed.path == "/api/sensors":
            self._send_json(self._snapshot_sensors())
            return
        if parsed.path == "/api/valves":
            self._send_json(self._snapshot_valves())
            return

        # Serve static files (index.html, app.js, styles.css)
        file = Path(parsed.path.lstrip("/"))
        if parsed.path == "/":
            file = Path("index.html")
        if file.exists():
            return super().do_GET()

        self.send_response(404)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(b"not found")

    def do_PUT(self):
        parsed = urlparse(self.path)
        segments = [seg for seg in parsed.path.split("/") if seg]
        if len(segments) == 2 and segments[0] == "api":
            self.send_response(400)
            self.end_headers()
            return
        if len(segments) != 3 or segments[0] != "api":
            self.send_response(404)
            self.end_headers()
            return

        _, resource, resource_id = segments
        length = int(self.headers.get("Content-Length", "0"))
        body = self.rfile.read(length)
        try:
            payload = json.loads(body.decode("utf-8")) if body else {}
        except Exception:
            payload = {}

        with state_lock:
            if resource == "sensors":
                sensor = next((s for s in SENSORS if s["id"] == resource_id), None)
                if not sensor:
                    self.send_response(404)
                    self.end_headers()
                    return
                if "setpoint" in payload:
                    sensor["setpoint"] = float(payload["setpoint"])
                self._send_json(sensor)
                return
            if resource == "valves":
                valve = valve_by_id(resource_id)
                if not valve:
                    self.send_response(404)
                    self.end_headers()
                    return
                max_degrees = open_degrees_max(valve)
                if "openDegreesMax" in payload:
                    valve["openDegreesMax"] = max(1, int(payload["openDegreesMax"]))
                    max_degrees = open_degrees_max(valve)
                    valve["openDegrees"] = position_to_degrees(valve["position"], valve)
                if "openDegrees" in payload:
                    valve["openDegrees"] = clamp(int(payload["openDegrees"]), 0, open_degrees_max(valve))
                    valve["position"] = clamp(round(valve["openDegrees"] * 100 / max_degrees), 0, 100)
                elif "position" in payload:
                    valve["position"] = clamp(int(payload["position"]), 0, 100)
                    valve["openDegrees"] = position_to_degrees(valve["position"], valve)
                if "mode" in payload:
                    valve["mode"] = payload["mode"]
                if "targetTemp" in payload:
                    valve["targetTemp"] = float(payload["targetTemp"])
                valve["isOpen"] = valve["position"] > 0
                self._send_json(valve)
                return

        self.send_response(404)
        self.end_headers()

    def _snapshot_sensors(self):
        with state_lock:
            return [dict(item) for item in SENSORS]

    def _snapshot_valves(self):
        with state_lock:
            return [dict(item) for item in VALVES]


def main():
    calibrate_all_valves()
    threading.Thread(target=update_physics_loop, daemon=True).start()
    server = HTTPServer(("0.0.0.0", 8080), Handler)
    print("Server started: http://127.0.0.1:8080")
    server.serve_forever()


if __name__ == "__main__":
    main()
