"""
WifiSwitch: relay + temperature/humidity node.

Device reports to:
  POST /wifiswitch/reading

Device polls server for desired relay state and reboot:
  POST /wifiswitch/api/poll

SolarMon dashboard can control the device via:
  POST /wifiswitch/api/set_relay
  POST /wifiswitch/api/reboot
"""

import json
import threading

import pymysql
from flask import Blueprint, Response, request

from config import (
    MYSQL_DATABASE,
    MYSQL_HOST,
    MYSQL_PASSWORD,
    MYSQL_PORT,
    MYSQL_USER,
)

bp = Blueprint("wifiswitch", __name__)

_state_lock = threading.Lock()
_relay_desired = False
_switch_reboot_pending = False
_temp_rule_enabled_pending = {}  # unit_id -> bool
_wifi_switch_schema_checked = False
_identity_pending_by_unit_id = {}  # unit_id -> {"new_unit_id": str, "new_node_name": str}

DB_CONFIG = {
    "host": MYSQL_HOST,
    "port": MYSQL_PORT,
    "user": MYSQL_USER,
    "password": MYSQL_PASSWORD,
    "database": MYSQL_DATABASE,
    "cursorclass": pymysql.cursors.DictCursor,
}


def get_db():
    return pymysql.connect(**DB_CONFIG)


def ensure_wifi_switch_table(conn):
    global _wifi_switch_schema_checked
    with conn.cursor() as cur:
        cur.execute(
            """
            CREATE TABLE IF NOT EXISTS WifiSwitchStatus (
                id INT AUTO_INCREMENT PRIMARY KEY,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

                unit_id VARCHAR(16) NOT NULL,
                node_name VARCHAR(50) DEFAULT NULL,

                temp FLOAT NULL,
                humid FLOAT NULL,
                relay_on TINYINT(1) NOT NULL,
                temp_rule_enabled TINYINT(1) NOT NULL DEFAULT 0,

                mac VARCHAR(24) NULL,
                ip VARCHAR(45) NULL,

                firmware_version VARCHAR(24) NULL
            )
            """
        )
    conn.commit()

    # ALTER TABLE safeguard for existing installations missing the column.
    with _state_lock:
        if _wifi_switch_schema_checked:
            return
        with conn.cursor() as cur:
            cur.execute(
                """
                SELECT 1
                FROM information_schema.columns
                WHERE table_schema=%s
                  AND table_name='WifiSwitchStatus'
                  AND column_name='temp_rule_enabled'
                LIMIT 1
                """,
                (MYSQL_DATABASE,),
            )
            has_col = cur.fetchone() is not None
            if not has_col:
                cur.execute(
                    "ALTER TABLE WifiSwitchStatus "
                    "ADD COLUMN temp_rule_enabled TINYINT(1) NOT NULL DEFAULT 0"
                )
                conn.commit()
        _wifi_switch_schema_checked = True


def _pop_temp_rule_enabled(unit_id: str):
    with _state_lock:
        if unit_id in _temp_rule_enabled_pending:
            return bool(_temp_rule_enabled_pending.pop(unit_id))
        return None


def _pop_identity_update(unit_id: str):
  """Pop one-time identity update for a unit_id."""
  with _state_lock:
    if unit_id in _identity_pending_by_unit_id:
      return _identity_pending_by_unit_id.pop(unit_id)
    return None


def _parse_bool(v):
    if isinstance(v, bool):
        return v
    if isinstance(v, (int, float)):
        return v != 0
    if isinstance(v, str):
        s = v.strip().lower()
        return s in ("1", "true", "on", "yes")
    return None


def _pop_reboot_command():
    """Pop pending reboot once; return 'reboot' or 'nosleep'."""
    global _switch_reboot_pending
    with _state_lock:
        if _switch_reboot_pending:
            _switch_reboot_pending = False
            return "reboot"
        return "nosleep"


def _get_desired_relay():
    with _state_lock:
        return bool(_relay_desired)


@bp.route("/reading", methods=["POST"])
def reading():
    """Accept relay + AHT10 reading and store in WifiSwitchStatus."""
    try:
        data = request.get_json(force=True, silent=True)
        if not data:
            return Response('{"ok":false,"error":"Invalid or missing JSON"}', status=400, mimetype="application/json")

        unit_id = data.get("unit_id")
        node_name = data.get("node_name")
        temp = data.get("temp")
        humid = data.get("humid")
        relay_on = data.get("relay_on")
        temp_rule_enabled = data.get("temp_rule_enabled")
        mac = data.get("mac")
        ip = data.get("ip")
        firmware_version = data.get("firmware_version")

        if unit_id is None or temp is None or humid is None or relay_on is None:
            return Response(
                '{"ok":false,"error":"Missing unit_id, temp, humid, or relay_on"}',
                status=400,
                mimetype="application/json",
            )

        if not isinstance(unit_id, str) or not unit_id.strip():
            return Response(
                '{"ok":false,"error":"unit_id must be a non-empty string"}',
                status=400,
                mimetype="application/json",
            )
        unit_id = unit_id.strip()[:16]

        node_name = (node_name if isinstance(node_name, str) else "").strip()
        node_name = node_name[:50] if node_name else None

        try:
            temp_f = float(temp)
        except (TypeError, ValueError):
            return Response(
                '{"ok":false,"error":"temp must be a number"}',
                status=400,
                mimetype="application/json",
            )

        try:
            humid_f = float(humid)
        except (TypeError, ValueError):
            return Response(
                '{"ok":false,"error":"humid must be a number"}',
                status=400,
                mimetype="application/json",
            )

        relay_on_b = _parse_bool(relay_on)
        if relay_on_b is None:
            return Response(
                '{"ok":false,"error":"relay_on must be boolean-like"}',
                status=400,
                mimetype="application/json",
            )

        temp_rule_enabled_b = _parse_bool(temp_rule_enabled)
        # Older device firmware may not send this field; default disabled.
        if temp_rule_enabled_b is None:
            temp_rule_enabled_b = False

        mac = (mac if isinstance(mac, str) else "").strip()[:24] or None
        ip = (ip if isinstance(ip, str) else "").strip()[:45] or None
        firmware_version = (
            (firmware_version if isinstance(firmware_version, str) else "").strip()[:24] or None
        )

        conn = get_db()
        try:
            ensure_wifi_switch_table(conn)
            with conn.cursor() as cur:
                cur.execute(
                    """
                    INSERT INTO WifiSwitchStatus
                        (unit_id, node_name, temp, humid, relay_on, temp_rule_enabled, mac, ip, firmware_version)
                    VALUES
                        (%s, %s, %s, %s, %s, %s, %s, %s, %s)
                    """,
                    (
                        unit_id,
                        node_name,
                        temp_f,
                        humid_f,
                        1 if relay_on_b else 0,
                        1 if temp_rule_enabled_b else 0,
                        mac,
                        ip,
                        firmware_version,
                    ),
                )
            conn.commit()
        finally:
            conn.close()

        command = _pop_reboot_command()
        desired_relay = _get_desired_relay()
        body = json.dumps(
            {"ok": True, "command": command, "relay_on": desired_relay},
            separators=(",", ":"),
        )
        return Response(body, status=200, mimetype="application/json")

    except pymysql.Error:
        return Response('{"ok":false,"error":"Database error"}', status=500, mimetype="application/json")
    except Exception as e:
        return Response(json.dumps({"ok": False, "error": str(e)}, separators=(",", ":")), status=500, mimetype="application/json")


@bp.route("/api/poll", methods=["POST"])
def api_poll():
    """Return desired relay state and one-time reboot command."""
    try:
        data = request.get_json(force=True, silent=True) or {}
        unit_id = data.get("unit_id")
        relay_on = data.get("relay_on")  # reported by device; we don't trust it for control
        firmware_version = data.get("firmware_version")

        if unit_id is None or (not isinstance(unit_id, str)) or not unit_id.strip():
            return Response('{"ok":false,"error":"unit_id required"}', status=400, mimetype="application/json")

        unit_id = unit_id.strip()[:16]

        # If needed later, we can validate unit_id length / firmware_version format.
        _ = relay_on
        _ = firmware_version

        command = _pop_reboot_command()
        desired_relay = _get_desired_relay()
        temp_rule_enabled = _pop_temp_rule_enabled(unit_id)
        identity_update = _pop_identity_update(unit_id)
        resp_obj = {"ok": True, "command": command, "relay_on": desired_relay}
        if temp_rule_enabled is not None:
            resp_obj["temp_rule_enabled"] = temp_rule_enabled
        if identity_update is not None:
            resp_obj["new_unit_id"] = identity_update.get("new_unit_id")
            resp_obj["new_node_name"] = identity_update.get("new_node_name")
        body = json.dumps(resp_obj, separators=(",", ":"))
        # Debug: show current desired relay each poll
        print(f"[WifiSwitch] api_poll unit_id={unit_id} desired_relay={desired_relay} cmd={command}", flush=True)
        return Response(body, status=200, mimetype="application/json")

    except Exception as e:
        return Response(json.dumps({"ok": False, "error": str(e)}, separators=(",", ":")), status=500, mimetype="application/json")


@bp.route("/api/set_relay", methods=["POST"])
def api_set_relay():
    """Set desired relay on/off (active-high relay)."""
    try:
        # Parse JSON (force=True) so we still decode if browser didn't set content-type perfectly.
        data = request.get_json(force=True, silent=True) or {}
        relay_on = data.get("relay_on")
        relay_on_b = _parse_bool(relay_on)
        if relay_on_b is None:
            return Response('{"ok":false,"error":"relay_on required (true/false)"}', status=400, mimetype="application/json")

        global _relay_desired
        with _state_lock:
            before = bool(_relay_desired)
            _relay_desired = bool(relay_on_b)
            after = bool(_relay_desired)
        # Server-side debug: confirm the toggle request was received.
        # (Flush immediately so you can see it in the server console right after clicking.)
        raw = request.get_data(cache=False)  # bytes
        raw_preview = raw[:200].decode("utf-8", errors="replace") if raw else ""
        print(
            f"[WifiSwitch] api_set_relay received={data} parsed_relay_on={after} (before={before}) "
            f"ct={request.content_type} raw_preview={raw_preview!r}",
            flush=True,
        )

        return Response(
            json.dumps({"ok": True, "relay_on": after}, separators=(",", ":")),
            status=200,
            mimetype="application/json",
        )

    except Exception as e:
        return Response(json.dumps({"ok": False, "error": str(e)}, separators=(",", ":")), status=500, mimetype="application/json")


@bp.route("/api/set_temp_rule_enabled", methods=["POST"])
def api_set_temp_rule_enabled():
    """Set a pending temp_rule_enabled flag for the device.

    Device will consume it once on the next poll and persist it to EEPROM.
    """
    try:
        data = request.get_json(force=True, silent=True) or {}
        unit_id = data.get("unit_id")
        temp_rule_enabled = data.get("temp_rule_enabled")

        if unit_id is None or (not isinstance(unit_id, str)) or not unit_id.strip():
            return Response(
                '{"ok":false,"error":"unit_id required"}',
                status=400,
                mimetype="application/json",
            )
        unit_id = unit_id.strip()[:16]

        enabled_b = _parse_bool(temp_rule_enabled)
        if enabled_b is None:
            return Response(
                '{"ok":false,"error":"temp_rule_enabled must be boolean-like"}',
                status=400,
                mimetype="application/json",
            )

        with _state_lock:
            _temp_rule_enabled_pending[unit_id] = bool(enabled_b)

        return Response('{"ok":true}', status=200, mimetype="application/json")

    except Exception as e:
        return Response(
            json.dumps({"ok": False, "error": str(e)}, separators=(",", ":")),
            status=500,
            mimetype="application/json",
        )


@bp.route("/api/set_node_id_name", methods=["POST"])
def api_set_node_id_name():
    """Set pending node identity for this WifiSwitch.

    Expects JSON:
      { "unit_id": "<current_id>", "new_unit_id": "<new_id>", "new_node_name": "<name>" }

    Device will consume it once on the next poll and persist it to EEPROM.
    """
    try:
        data = request.get_json(force=True, silent=True) or {}
        unit_id = data.get("unit_id")
        new_unit_id = data.get("new_unit_id")
        new_node_name = data.get("new_node_name")

        if unit_id is None or (not isinstance(unit_id, str)) or not unit_id.strip():
            return Response(
                '{"ok":false,"error":"unit_id required"}',
                status=400,
                mimetype="application/json",
            )
        if new_unit_id is None or (not isinstance(new_unit_id, str)) or not new_unit_id.strip():
            return Response(
                '{"ok":false,"error":"new_unit_id required"}',
                status=400,
                mimetype="application/json",
            )
        if new_node_name is None or (not isinstance(new_node_name, str)) or not new_node_name.strip():
            return Response(
                '{"ok":false,"error":"new_node_name required"}',
                status=400,
                mimetype="application/json",
            )

        unit_id = unit_id.strip()[:16]
        new_unit_id = new_unit_id.strip()[:16]
        new_node_name = new_node_name.strip()[:50]

        with _state_lock:
            _identity_pending_by_unit_id[unit_id] = {
                "new_unit_id": new_unit_id,
                "new_node_name": new_node_name,
            }

        return Response('{"ok":true}', status=200, mimetype="application/json")

    except Exception as e:
        return Response(
            json.dumps({"ok": False, "error": str(e)}, separators=(",", ":")),
            status=500,
            mimetype="application/json",
        )


@bp.route("/api/reboot", methods=["POST"])
def api_reboot():
    """Set pending reboot; device will reboot on next poll/reading."""
    global _switch_reboot_pending
    with _state_lock:
        _switch_reboot_pending = True
    print("[WifiSwitch] api_reboot pending set", flush=True)
    return Response('{"ok":true}', status=200, mimetype="application/json")

