import asyncio
import errno
import json
import os
import secrets
import signal
from microdot import Microdot, send_file
from microdot.websocket import with_websocket
from microdot.session import Session
from settings import get_settings

import controllers.preset as preset
import controllers.profile as profile
import controllers.group as group
import controllers.sequence as sequence
import controllers.zone as zone
import controllers.palette as palette
import controllers.scene as scene
import controllers.pattern as pattern
import controllers.settings as settings_controller
import controllers.device as device_controller
import controllers.led_tool as led_tool_controller
from models.transport import get_sender, set_sender, get_current_sender
from models.device import Device
from models.bridge_ws_client import init_bridge_client
from util.espnow_registry import handle_espnow_announce
from util.binary_driver_messages import v1_dict_to_cmd_packet
from util.audio_detector import AudioBeatDetector


def _live_reload_enabled() -> bool:
    v = os.environ.get("LED_CONTROLLER_LIVE_RELOAD", "").strip().lower()
    return v not in ("", "0", "false", "no")


async def main(port=80):
    settings = get_settings()
    print(settings)
    print("Starting")

    sender = get_sender(settings)
    set_sender(sender)

    bridge_url = str(settings.get("bridge_ws_url") or "").strip()
    if bridge_url:
        try:
            ch = int(settings.get("wifi_channel", 6))
        except (TypeError, ValueError):
            ch = 6
        bridge = init_bridge_client(bridge_url, wifi_channel=ch)
        bridge.set_uplink_handler(handle_espnow_announce)
        bridge.start()

    app = Microdot()
    audio_detector = AudioBeatDetector()
    try:
        from util import audio_detector as audio_detector_module

        audio_detector_module.set_shared_beat_detector(audio_detector)
    except Exception as e:
        print(f"[startup] audio detector shared registration skipped: {e!r}")
    try:
        from util.audio_run_persist import coerce_audio_device, read_audio_run_state

        persisted = read_audio_run_state()
        if persisted.get("enabled"):
            dev = coerce_audio_device(persisted.get("device"))
            audio_detector.start(device=dev)
            print("[startup] audio beat detector started from saved run state")
    except Exception as e:
        print(f"[startup] audio auto-start skipped: {e!r}")
    from util import beat_driver_route

    beat_driver_route.set_beat_route_main_loop(asyncio.get_running_loop())
    from util import sequence_playback as seq_pb

    seq_pb.ensure_beat_consumer_started()
    
    # Initialize sessions with a secret key from settings
    secret_key = settings.get('session_secret_key', 'led-controller-secret-key-change-in-production')
    Session(app, secret_key=secret_key)
    
    # Mount model controllers as subroutes
    # Verify controllers are Microdot instances before mounting
    controllers_to_mount = [
        ('/presets', preset, 'preset'),
        ('/profiles', profile, 'profile'),
        ('/groups', group, 'group'),
        ('/sequences', sequence, 'sequence'),
        ('/zones', zone, 'zone'),
        ('/palettes', palette, 'palette'),
        ('/scenes', scene, 'scene'),
    ]
    
    # Mount model controllers as subroutes
    app.mount(preset.controller, '/presets')
    app.mount(profile.controller, '/profiles')
    app.mount(group.controller, '/groups')
    app.mount(sequence.controller, '/sequences')
    app.mount(zone.controller, '/zones')
    app.mount(palette.controller, '/palettes')
    app.mount(scene.controller, '/scenes')
    app.mount(pattern.controller, '/patterns')
    app.mount(settings_controller.controller, '/settings')
    app.mount(device_controller.controller, '/devices')
    app.mount(led_tool_controller.controller, '/led-tool')

    live_reload = _live_reload_enabled()
    dev_build_id = secrets.token_hex(12) if live_reload else None
    if live_reload:
        print(
            "[dev] LED_CONTROLLER_LIVE_RELOAD: browser refreshes when the server process restarts"
        )

    if dev_build_id:

        @app.route("/__dev/build-id")
        def dev_build_id_route(request):
            _ = request
            return (
                dev_build_id,
                200,
                {
                    "Content-Type": "text/plain; charset=utf-8",
                    "Cache-Control": "no-store",
                },
            )

    # Serve index.html at root (cwd is src/ when run via pipenv run run)
    @app.route("/")
    def index(request):
        """Serve the main web UI."""
        if dev_build_id:
            try:
                with open("templates/index.html", encoding="utf-8") as f:
                    html = f.read()
                tag = '<script src="/static/dev-live-reload.js" defer></script>'
                if "</body>" in html:
                    html = html.replace("</body>", tag + "\n</body>", 1)
                    return html, 200, {"Content-Type": "text/html; charset=utf-8"}
            except OSError:
                pass
        return send_file("templates/index.html")

    # Favicon: avoid 404 in browser console (no file needed)
    @app.route('/favicon.ico')
    def favicon(request):
        return '', 204

    @app.route('/api/audio/devices')
    async def audio_devices(request):
        _ = request
        try:
            return {
                "devices": audio_detector.list_input_devices(),
                "diagnostics": audio_detector.diagnostics(),
            }
        except Exception as e:
            return {"error": str(e)}, 500

    @app.route('/api/audio/start', methods=['POST'])
    async def audio_start(request):
        payload = request.json if isinstance(request.json, dict) else {}
        device = payload.get("device", None)
        if device in ("", None):
            device = None
        else:
            try:
                device = int(device)
            except (TypeError, ValueError):
                pass
        try:
            audio_detector.start(device=device)
            from util.audio_run_persist import write_audio_run_state

            write_audio_run_state(
                enabled=True,
                device=device,
                device_override=str(payload.get("device_override") or ""),
                device_select=str(payload.get("device_select") or ""),
            )
            return {"ok": True, "status": audio_detector.status()}
        except Exception as e:
            return {"ok": False, "error": str(e)}, 500

    @app.route('/api/audio/stop', methods=['POST'])
    async def audio_stop(request):
        _ = request
        audio_detector.stop()
        from util.audio_run_persist import write_audio_run_state

        write_audio_run_state(enabled=False)
        return {"ok": True, "status": audio_detector.status()}

    @app.route('/api/audio/reset', methods=['POST'])
    async def audio_reset(request):
        """Clear beat/BPM tracking state without stopping the detector."""
        _ = request
        ok = audio_detector.reset_tracking()
        if not ok:
            return {"ok": False, "error": "Audio detector is not running"}, 409
        return {"ok": True, "status": audio_detector.status()}

    @app.route('/api/audio/anchor-bar', methods=['POST'])
    async def audio_anchor_bar(request):
        """Mark the current moment as bar beat 1 (downbeat)."""
        _ = request
        ok = audio_detector.anchor_bar_phase()
        if not ok:
            return {"ok": False, "error": "Audio detector is not running"}, 409
        return {"ok": True, "status": audio_detector.status()}

    @app.route('/api/audio/status')
    async def audio_status(request):
        _ = request
        from util import beat_driver_route
        from util import sequence_playback

        st = audio_detector.status()
        st["sequence"] = sequence_playback.playback_status()
        st["manual_beat_stride"] = beat_driver_route.manual_beat_stride_status()
        seq = st.get("sequence")
        beat_readout = ""
        if isinstance(seq, dict) and str(seq.get("beat_readout") or "").strip():
            beat_readout = str(seq.get("beat_readout") or "").strip()
        elif st.get("running"):
            mb = st.get("manual_beat_stride")
            if isinstance(mb, dict) and mb.get("active"):
                try:
                    n = int(mb.get("stride_n") or 1)
                except (TypeError, ValueError):
                    n = 1
                n = max(1, min(64, n))
                try:
                    bi = int(mb.get("beat_in_stride") or 1)
                except (TypeError, ValueError):
                    bi = 1
                pos = min(n, max(1, bi))
                beat_readout = f"{pos}/{n}"
            else:
                try:
                    bs = int(st.get("beat_seq") or 0)
                except (TypeError, ValueError):
                    bs = 0
                if bs > 0:
                    beat_readout = str(bs)
        st["beat_readout"] = beat_readout
        from util.audio_run_persist import read_audio_run_state

        st["beat_phase_ms"] = int(settings.get("audio_beat_phase_ms") or 0)
        seq_wait = str(settings.get("sequence_switch_wait") or "beat").strip().lower()
        if seq_wait not in ("beat", "downbeat"):
            seq_wait = "beat"
        st["sequence_switch_wait"] = seq_wait
        st["audio_run"] = read_audio_run_state()
        return {"status": st}

    # Static file route
    @app.route("/static/<path:path>")
    def static_handler(request, path):
        """Serve static files."""
        if '..' in path:
            # Directory traversal is not allowed
            return 'Not found', 404
        return send_file('static/' + path)

    @app.route('/ws')
    @with_websocket
    async def ws(request, ws):
        try:
            while True:
                data = await ws.receive()
                if not data:
                    break
                try:
                    if isinstance(data, (bytes, bytearray)):
                        await sender.send(bytes(data))
                        continue
                    parsed = json.loads(data)
                    addr = parsed.pop("to", None)
                    pkt = v1_dict_to_cmd_packet(parsed)
                    await sender.send(pkt, addr=addr)
                except json.JSONDecodeError:
                    pass
                except Exception:
                    try:
                        await ws.send(json.dumps({"error": "Send failed"}))
                    except Exception:
                        pass
        except Exception:
            pass

    Device()
    loop = asyncio.get_running_loop()
    server_tasks: list[asyncio.Task] = []

    def _graceful_shutdown(*_args):
        print("[server] shutting down...")
        try:
            audio_detector.stop()
        except Exception:
            pass
        try:
            from util import sequence_playback as seq_pb

            seq_pb.stop()
            for attr in ("_pending_beat_task", "_sim_beat_task"):
                t = getattr(seq_pb, attr, None)
                if t is not None and not t.done():
                    t.cancel()
        except Exception:
            pass
        if getattr(app, "server", None) is not None:
            try:
                app.shutdown()
            except Exception:
                pass
        for t in server_tasks:
            if not t.done():
                t.cancel()

    shutdown_handlers_registered = False
    try:
        try:
            for sig in (signal.SIGINT, signal.SIGTERM):
                loop.add_signal_handler(sig, _graceful_shutdown)
            shutdown_handlers_registered = True
        except (NotImplementedError, RuntimeError):
            pass

        try:
            server_tasks[:] = [
                asyncio.create_task(
                    app.start_server(host="0.0.0.0", port=port), name="http"
                ),
            ]
            await asyncio.gather(*server_tasks)
        except asyncio.CancelledError:
            pass
        except OSError as e:
            if e.errno == errno.EADDRINUSE:
                print(
                    f"[server] bind failed (address already in use): {e!s}\n"
                    f"[server] HTTP is configured for port {port} (env PORT). "
                    f"Stop the other process or use a free port, e.g. PORT=8080 pipenv run run"
                )
            raise
    finally:
        try:
            audio_detector.stop()
        except Exception:
            pass
        srv = getattr(app, "server", None)
        if srv is not None:
            try:
                srv.close()
                await srv.wait_closed()
            except Exception:
                pass
            try:
                app.server = None
            except Exception:
                pass
        for t in list(server_tasks):
            if not t.done():
                t.cancel()
        if server_tasks:
            await asyncio.gather(*server_tasks, return_exceptions=True)
        pending = [
            t
            for t in asyncio.all_tasks(loop)
            if t is not asyncio.current_task() and not t.done()
        ]
        for t in pending:
            t.cancel()
        if pending:
            await asyncio.gather(*pending, return_exceptions=True)
        if shutdown_handlers_registered:
            for sig in (signal.SIGINT, signal.SIGTERM):
                try:
                    loop.remove_signal_handler(sig)
                except (NotImplementedError, OSError, ValueError):
                    pass

if __name__ == "__main__":
    import os

    port = int(os.environ.get("PORT", 80))
    try:
        asyncio.run(main(port=port))
    except KeyboardInterrupt:
        print("[server] interrupted")
