from microdot import Microdot
from microdot.session import with_session
import asyncio
from models.group import Group
from models.device import Device
from models.transport import get_current_sender
from util.binary_driver_messages import v1_dict_to_cmd_packet
from util.espnow_registry import push_groups_for_group_devices
from settings import get_settings
from util.brightness_combine import effective_brightness_for_mac
import json

controller = Microdot()
groups = Group()
devices = Device()
_pi_settings = get_settings()


def _group_doc_visible_for_profile(doc, profile_id):
    if not isinstance(doc, dict):
        return False
    scoped = doc.get("profile_id")
    if scoped is None:
        scoped = doc.get("profileId")
    if scoped is None or str(scoped).strip() == "":
        return True
    if not profile_id:
        return False
    return str(scoped).strip() == str(profile_id).strip()


def _filtered_groups_dict(session):
    from controllers.zone import get_current_profile_id

    pid = get_current_profile_id(session)
    out = {}
    for gid, doc in groups.items():
        if not isinstance(doc, dict):
            continue
        if _group_doc_visible_for_profile(doc, pid):
            out[str(gid)] = doc
    return out


@controller.get("")
@with_session
async def list_groups(request, session):
    """List groups visible for the current profile (shared + profile-scoped)."""
    return json.dumps(_filtered_groups_dict(session)), 200, {"Content-Type": "application/json"}


@controller.get("/<id>")
@with_session
async def get_group(request, session, id):
    """Get a specific group by ID (404 if scoped to another profile)."""
    group = groups.read(id)
    if not group or not isinstance(group, dict):
        return json.dumps({"error": "Group not found"}), 404
    from controllers.zone import get_current_profile_id

    if not _group_doc_visible_for_profile(group, get_current_profile_id(session)):
        return json.dumps({"error": "Group not found"}), 404
    return json.dumps(group), 200, {"Content-Type": "application/json"}


def _sanitize_group_profile_id_write(data, session):
    """Allow ``profile_id`` only for the active profile, or null to share across profiles."""
    if not isinstance(data, dict):
        return
    from controllers.zone import get_current_profile_id

    cur = get_current_profile_id(session)
    if "profile_id" not in data and "profileId" not in data:
        return
    raw = data.get("profile_id")
    if raw is None and "profileId" in data:
        raw = data.get("profileId")
    if raw is None or raw == "":
        data.pop("profileId", None)
        data["profile_id"] = None
        return
    if not cur or str(raw).strip() != str(cur).strip():
        data.pop("profileId", None)
        data.pop("profile_id", None)


@controller.post("")
@with_session
async def create_group(request, session):
    """Create a new group (omit ``profile_id`` for shared; or ``profile_scoped``: true for this profile only)."""
    try:
        data = dict(request.json or {})
        name = data.get("name", "")
        profile_scoped = bool(data.pop("profile_scoped", False))
        _sanitize_group_profile_id_write(data, session)
        group_id = groups.create(name)
        if data:
            groups.update(group_id, data)
        if profile_scoped:
            from controllers.zone import get_current_profile_id

            cur = get_current_profile_id(session)
            if cur:
                groups.update(group_id, {"profile_id": str(cur)})
        g = groups.read(group_id)
        if g:
            await push_groups_for_group_devices(g)
        return json.dumps(groups.read(group_id)), 201, {"Content-Type": "application/json"}
    except Exception as e:
        return json.dumps({"error": str(e)}), 400


@controller.put("/<id>")
@with_session
async def update_group(request, session, id):
    """Update an existing group."""
    try:
        data = request.json
        if not isinstance(data, dict):
            return json.dumps({"error": "Invalid JSON"}), 400, {"Content-Type": "application/json"}
        data = dict(data)
        _sanitize_group_profile_id_write(data, session)
        if groups.update(id, data):
            g = groups.read(id)
            if g:
                await push_groups_for_group_devices(g)
                return json.dumps(g), 200, {"Content-Type": "application/json"}
        return json.dumps({"error": "Group not found"}), 404
    except Exception as e:
        return json.dumps({"error": str(e)}), 400

@controller.delete("/<id>")
@with_session
async def delete_group(request, session, id):
    """Delete a group (not allowed for another profile's scoped group)."""
    g = groups.read(id)
    if not g or not isinstance(g, dict):
        return json.dumps({"error": "Group not found"}), 404
    from controllers.zone import get_current_profile_id

    if not _group_doc_visible_for_profile(g, get_current_profile_id(session)):
        return json.dumps({"error": "Group not found"}), 404
    macs = list(g.get("devices") or []) if isinstance(g, dict) else []
    if groups.delete(id):
        await push_groups_for_group_devices({"devices": macs})
        return json.dumps({"message": "Group deleted successfully"}), 200
    return json.dumps({"error": "Group not found"}), 404


def _group_driver_config_payload(doc):
    """Build ``device_config`` dict from stored group Wi‑Fi defaults (non-empty only)."""
    dc = {}
    if not isinstance(doc, dict):
        return dc
    nm = doc.get("wifi_driver_display_name")
    if isinstance(nm, str) and nm.strip():
        dc["name"] = nm.strip()
    nled = doc.get("wifi_driver_num_leds")
    if nled is not None:
        try:
            n = int(nled)
            if 1 <= n <= 2048:
                dc["num_leds"] = n
        except (TypeError, ValueError):
            pass
    co = doc.get("wifi_color_order")
    if isinstance(co, str):
        c = co.strip().lower()
        if c in ("rgb", "rbg", "grb", "gbr", "brg", "bgr"):
            dc["color_order"] = c
    sm = doc.get("wifi_startup_mode")
    if isinstance(sm, str):
        s = sm.strip().lower()
        if s in ("default", "last", "off"):
            dc["startup_mode"] = s
    return dc


def _read_group_for_session(session, id):
    g = groups.read(id)
    if not g or not isinstance(g, dict):
        return None
    from controllers.zone import get_current_profile_id

    if not _group_doc_visible_for_profile(g, get_current_profile_id(session)):
        return None
    return g


@controller.post("/<id>/driver-config")
@with_session
async def push_group_driver_config(request, session, id):
    """
    Push group driver defaults to every ESP-NOW device listed in the group.
    Uses stored ``wifi_*`` fields on the group; optional JSON body may override for this send only.
    """
    gdoc = _read_group_for_session(session, id)
    if not gdoc:
        return json.dumps({"error": "Group not found"}), 404

    body = request.json or {}
    merged = dict(gdoc)
    if isinstance(body, dict):
        for k in (
            "wifi_driver_display_name",
            "wifi_driver_num_leds",
            "wifi_color_order",
            "wifi_startup_mode",
        ):
            if k in body:
                merged[k] = body[k]
    dc = _group_driver_config_payload(merged)
    if not dc:
        return json.dumps(
            {"error": "No driver defaults on this group (set display name, LEDs, colour order, or power-on pattern)"}
        ), 400, {"Content-Type": "application/json"}

    mac_list = gdoc.get("devices") if isinstance(gdoc.get("devices"), list) else []
    sent = 0
    errors = []
    sender = get_current_sender()
    if not sender:
        return json.dumps({"error": "Transport not configured"}), 503
    pkt = v1_dict_to_cmd_packet({"v": "1", "device_config": dc, "save": True})
    for mac in mac_list:
        m = str(mac).strip().lower().replace(":", "").replace("-", "")
        if len(m) != 12:
            continue
        dev = devices.read(m)
        if not dev:
            errors.append({"mac": m, "error": "not in registry"})
            continue
        try:
            if await sender.send(pkt, addr=m):
                sent += 1
            else:
                errors.append({"mac": m, "error": "send failed"})
        except Exception as e:
            errors.append({"mac": m, "error": str(e)})

    return json.dumps(
        {"message": "driver-config sent", "sent": sent, "errors": errors}
    ), 200, {"Content-Type": "application/json"}


def _brightness_save_message_json(b_val: int) -> str:
    b_val = max(0, min(255, int(b_val)))
    return json.dumps({"v": "1", "b": b_val, "save": True}, separators=(",", ":"))


@controller.post("/<id>/brightness")
@with_session
async def push_group_output_brightness(request, session, id):
    """
    Push combined brightness (global × group(s) × device) to each member — one ``b`` per device.
    """
    gdoc = _read_group_for_session(session, id)
    if not gdoc:
        return json.dumps({"error": "Group not found"}), 404

    mac_list = gdoc.get("devices") if isinstance(gdoc.get("devices"), list) else []
    sent = 0
    errors = []
    sender = get_current_sender()

    async def _push_brightness_one(m: str, dev: dict) -> tuple[str, bool, str | None]:
        b_val = effective_brightness_for_mac(
            _pi_settings,
            groups,
            devices,
            m,
            zone_brightness=None,
        )
        pkt = v1_dict_to_cmd_packet(
            {"v": "1", "b": b_val, "save": True},
        )
        if not sender:
            return m, False, "transport not configured"
        try:
            ok = await sender.send(pkt, addr=m)
            return m, bool(ok), None if ok else "send failed"
        except Exception as e:
            return m, False, str(e)

    tasks: list = []
    for mac in mac_list:
        m = str(mac).strip().lower().replace(":", "").replace("-", "")
        if len(m) != 12:
            continue
        dev = devices.read(m)
        if not dev:
            errors.append({"mac": m, "error": "not in registry"})
            continue
        tasks.append(_push_brightness_one(m, dev))

    if tasks:
        results = await asyncio.gather(*tasks, return_exceptions=True)
        for r in results:
            if isinstance(r, Exception):
                errors.append({"mac": "*", "error": str(r)})
                continue
            m, ok, err = r
            if ok:
                sent += 1
            elif err:
                errors.append({"mac": m, "error": err})

    return json.dumps(
        {"message": "brightness sent", "sent": sent, "errors": errors}
    ), 200, {"Content-Type": "application/json"}


@controller.post("/<id>/identify")
@with_session
async def identify_group_devices(request, session, id):
    """
    Run the same identify blink as ``POST /devices/<id>/identify`` for every registry member
    in parallel so all drivers in the group blink together.
    """
    _ = request
    gdoc = _read_group_for_session(session, id)
    if not gdoc:
        return json.dumps({"error": "Group not found"}), 404, {"Content-Type": "application/json"}

    mac_list = gdoc.get("devices") if isinstance(gdoc.get("devices"), list) else []
    if not mac_list:
        return json.dumps({"error": "Group has no devices"}), 400, {"Content-Type": "application/json"}

    from controllers.device import send_identify_to_group_devices

    normalized: list[str] = []
    errors: list[dict] = []
    for mac in mac_list:
        m = str(mac).strip().lower().replace(":", "").replace("-", "")
        if len(m) != 12:
            errors.append({"mac": str(mac), "error": "invalid MAC"})
            continue
        normalized.append(m)

    if not normalized:
        return json.dumps(
            {"message": "identify group done", "sent": 0, "errors": errors}
        ), 200, {"Content-Type": "application/json"}

    sent, batch_errors = await send_identify_to_group_devices(normalized)
    errors.extend(batch_errors)

    return json.dumps(
        {"message": "identify group done", "sent": sent, "errors": errors}
    ), 200, {"Content-Type": "application/json"}
