from microdot import Microdot
from microdot.session import with_session
from models.preset import Preset
from models.profile import Profile
from models.device import Device, normalize_mac
from models.transport import get_current_sender
from util.driver_delivery import deliver_json_messages, deliver_preset_broadcast_then_per_device
from util.espnow_message import build_message, build_preset_dict
import json

controller = Microdot()
presets = Preset()
profiles = Profile()

def get_current_profile_id(session=None):
    """Get the current active profile ID from session or fallback to first."""
    profile_list = profiles.list()
    session_profile = None
    if session is not None:
        session_profile = session.get('current_profile')
    if session_profile and session_profile in profile_list:
        return session_profile
    if profile_list:
        return profile_list[0]
    return None

@controller.get('')
@with_session
async def list_presets(request, session):
    """List presets for the current profile."""
    current_profile_id = get_current_profile_id(session)
    if not current_profile_id:
        return json.dumps({}), 200, {'Content-Type': 'application/json'}
    scoped = {
        pid: pdata for pid, pdata in presets.items()
        if isinstance(pdata, dict) and str(pdata.get("profile_id")) == str(current_profile_id)
    }
    return json.dumps(scoped), 200, {'Content-Type': 'application/json'}

@controller.get('/<preset_id>')
@with_session
async def get_preset(request, session, preset_id):
    """Get a specific preset by ID (current profile only)."""
    preset = presets.read(preset_id)
    current_profile_id = get_current_profile_id(session)
    if preset and str(preset.get("profile_id")) == str(current_profile_id):
        return json.dumps(preset), 200, {'Content-Type': 'application/json'}
    return json.dumps({"error": "Preset not found"}), 404

@controller.post('')
@with_session
async def create_preset(request, session):
    """Create a new preset for the current profile."""
    try:
        try:
            data = request.json or {}
        except Exception:
            return json.dumps({"error": "Invalid JSON"}), 400, {'Content-Type': 'application/json'}
        current_profile_id = get_current_profile_id(session)
        if not current_profile_id:
            return json.dumps({"error": "No profile available"}), 404
        preset_id = presets.create(current_profile_id)
        if not isinstance(data, dict):
            data = {}
        data = dict(data)
        data["profile_id"] = str(current_profile_id)
        if presets.update(preset_id, data):
            preset_data = presets.read(preset_id)
            return json.dumps({preset_id: preset_data}), 201, {'Content-Type': 'application/json'}
        return json.dumps({"error": "Failed to create preset"}), 400
    except Exception as e:
        return json.dumps({"error": str(e)}), 400

@controller.put('/<preset_id>')
@with_session
async def update_preset(request, session, preset_id):
    """Update an existing preset (current profile only)."""
    try:
        preset = presets.read(preset_id)
        current_profile_id = get_current_profile_id(session)
        if not preset or str(preset.get("profile_id")) != str(current_profile_id):
            return json.dumps({"error": "Preset not found"}), 404
        try:
            data = request.json or {}
        except Exception:
            return json.dumps({"error": "Invalid JSON"}), 400, {'Content-Type': 'application/json'}
        if not isinstance(data, dict):
            data = {}
        data = dict(data)
        data["profile_id"] = str(current_profile_id)
        if presets.update(preset_id, data):
            return json.dumps(presets.read(preset_id)), 200, {'Content-Type': 'application/json'}
        return json.dumps({"error": "Preset not found"}), 404
    except Exception as e:
        return json.dumps({"error": str(e)}), 400

@controller.delete('/<preset_id>')
@with_session
async def delete_preset(request, *args, **kwargs):
    """Delete a preset (current profile only)."""
    # Be tolerant of wrapper/arg-order variations.
    session = None
    preset_id = None
    if len(args) > 0:
        session = args[0]
    if len(args) > 1:
        preset_id = args[1]
    if 'session' in kwargs and kwargs.get('session') is not None:
        session = kwargs.get('session')
    if 'preset_id' in kwargs and kwargs.get('preset_id') is not None:
        preset_id = kwargs.get('preset_id')
    if 'id' in kwargs and kwargs.get('id') is not None and preset_id is None:
        preset_id = kwargs.get('id')
    if preset_id is None:
        return json.dumps({"error": "Preset ID is required"}), 400
    preset = presets.read(preset_id)
    current_profile_id = get_current_profile_id(session)
    if not preset or str(preset.get("profile_id")) != str(current_profile_id):
        return json.dumps({"error": "Preset not found"}), 404
    if presets.delete(preset_id):
        return json.dumps({"message": "Preset deleted successfully"}), 200
    return json.dumps({"error": "Preset not found"}), 404


@controller.post('/send')
@with_session
async def send_presets(request, session):
    """
    Send one or more presets to LED drivers (serial/ESP-NOW and/or TCP Wi-Fi clients).

    Body JSON:
        {"preset_ids": ["1", "2", ...]}  or  {"ids": ["1", "2", ...]}
        Optional "targets": ["aabbccddeeff", ...] — registry MACs. When set: preset
        chunks are ESP-NOW broadcast once each; Wi-Fi drivers get the same chunks
        over TCP; if "default" is set, each target then gets a unicast default
        message (serial or TCP) with that device name in "targets".
        Omit targets for broadcast-only serial (legacy).

    Optional "destination_mac" / "to": single MAC when targets is omitted.
    """
    try:
        data = request.json or {}
    except Exception:
        return json.dumps({"error": "Invalid JSON"}), 400, {'Content-Type': 'application/json'}

    preset_ids = data.get('preset_ids') or data.get('ids')
    if not isinstance(preset_ids, list) or not preset_ids:
        return json.dumps({"error": "preset_ids must be a non-empty list"}), 400, {'Content-Type': 'application/json'}
    save_flag = data.get('save', True)
    save_flag = bool(save_flag)
    default_id = data.get('default')
    destination_mac = data.get('destination_mac') or data.get('to')

    # Build API-compliant preset map keyed by preset ID, include name
    current_profile_id = get_current_profile_id(session)
    presets_by_name = {}
    for pid in preset_ids:
        preset_data = presets.read(str(pid))
        if not preset_data:
            continue
        if str(preset_data.get("profile_id")) != str(current_profile_id):
            continue
        preset_key = str(pid)
        preset_payload = build_preset_dict(preset_data)
        preset_payload["name"] = preset_data.get("name", "")
        presets_by_name[preset_key] = preset_payload

    if not presets_by_name:
        return json.dumps({"error": "No matching presets found"}), 404, {'Content-Type': 'application/json'}

    if default_id is not None and str(default_id) not in presets_by_name:
        default_id = None

    sender = get_current_sender()
    if not sender:
        return json.dumps({"error": "Transport not configured"}), 503, {'Content-Type': 'application/json'}

    MAX_BYTES = 240
    send_delay_s = 0.1
    entries = list(presets_by_name.items())
    total_presets = len(entries)

    batch = {}
    chunk_messages = []
    for name, preset_obj in entries:
        test_batch = dict(batch)
        test_batch[name] = preset_obj
        test_msg = build_message(presets=test_batch, save=save_flag, default=default_id)
        size = len(test_msg)

        if size <= MAX_BYTES or not batch:
            batch = test_batch
        else:
            chunk_messages.append(
                build_message(
                    presets=dict(batch),
                    save=False,
                    default=None,
                )
            )
            batch = {name: preset_obj}

    if batch:
        chunk_messages.append(
            build_message(
                presets=dict(batch),
                save=save_flag,
                default=default_id,
            )
        )

    target_list = None
    raw_targets = data.get("targets")
    if isinstance(raw_targets, list) and raw_targets:
        target_list = []
        for t in raw_targets:
            m = normalize_mac(str(t))
            if m:
                target_list.append(m)
        target_list = list(dict.fromkeys(target_list))
        if not target_list:
            target_list = None
    elif destination_mac:
        dm = normalize_mac(str(destination_mac))
        target_list = [dm] if dm else None

    try:
        if target_list:
            deliveries = await deliver_preset_broadcast_then_per_device(
                sender,
                chunk_messages,
                target_list,
                Device(),
                str(default_id) if default_id is not None else None,
                delay_s=send_delay_s,
            )
        else:
            deliveries, _chunks = await deliver_json_messages(
                sender,
                chunk_messages,
                None,
                Device(),
                delay_s=send_delay_s,
            )
    except Exception:
        return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'}

    return json.dumps({
        "message": "Presets sent",
        "presets_sent": total_presets,
        "messages_sent": deliveries,
    }), 200, {'Content-Type': 'application/json'}


@controller.post('/push')
@with_session
async def push_driver_messages(request, session):
    """
    Deliver one or more raw v1 JSON objects to devices (ESP-NOW and/or TCP).

    Body:
        {"sequence": [{ "v": "1", ... }, ...], "targets": ["mac", ...]}
    or a single {"payload": {...}, "targets": [...]}.
    """
    try:
        data = request.json or {}
    except Exception:
        return json.dumps({"error": "Invalid JSON"}), 400, {'Content-Type': 'application/json'}

    seq = data.get("sequence")
    if not seq and data.get("payload") is not None:
        seq = [data["payload"]]
    if not isinstance(seq, list) or not seq:
        return json.dumps({"error": "sequence or payload required"}), 400, {'Content-Type': 'application/json'}

    raw_targets = data.get("targets")
    target_list = None
    if isinstance(raw_targets, list) and raw_targets:
        target_list = []
        for t in raw_targets:
            m = normalize_mac(str(t))
            if m:
                target_list.append(m)
        target_list = list(dict.fromkeys(target_list))
        if not target_list:
            target_list = None

    sender = get_current_sender()
    if not sender:
        return json.dumps({"error": "Transport not configured"}), 503, {'Content-Type': 'application/json'}

    messages = []
    for item in seq:
        if isinstance(item, dict):
            messages.append(json.dumps(item))
        elif isinstance(item, str):
            messages.append(item)
        else:
            return json.dumps({"error": "sequence items must be objects or strings"}), 400, {'Content-Type': 'application/json'}

    delay_s = data.get("delay_s", 0.05)
    try:
        delay_s = float(delay_s)
    except (TypeError, ValueError):
        delay_s = 0.05

    try:
        deliveries, _chunks = await deliver_json_messages(
            sender,
            messages,
            target_list,
            Device(),
            delay_s=delay_s,
        )
    except Exception:
        return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'}

    try:
        from util.beat_driver_route import sync_beat_route_from_push_sequence

        sync_beat_route_from_push_sequence(seq)
    except Exception:
        pass

    return json.dumps({
        "message": "Delivered",
        "deliveries": deliveries,
    }), 200, {'Content-Type': 'application/json'}

