import json
import os
import shutil
import subprocess
import time
import threading
import html

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs
import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs

ADDON_DIR = xbmcvfs.translatePath("special://home/addons/service.voicehelper")
DATA_DIR = xbmcvfs.translatePath("special://profile/addon_data/service.voicehelper")
STATE_FILE = os.path.join(DATA_DIR, "ui-state.json")
REMOTE_FILE = os.path.join(DATA_DIR, "remote.json")

def remote_is_configured():
    if configured_remote_mac():
        return True
    # Legacy remote.json remains only for service-side migration compatibility.
    return os.path.exists(REMOTE_FILE)

SETUP_LOCK = os.path.join(DATA_DIR, "setup.lock")

os.makedirs(DATA_DIR, exist_ok=True)

for name in ("remote-map.json", "button-actions.json", "config.json"):
    src = os.path.join(ADDON_DIR, name)
    dst = os.path.join(DATA_DIR, name)
    if not os.path.exists(dst):
        shutil.copyfile(src, dst)

# 1.0.17: Xiaomi TV+ is VoiceHelper's dedicated KodiFlix Info button.
# Existing installs may still have the old semantic Kodi "Info" action cached
# in addon_data. Replace only that historical value; preserve every other
# button assignment and any user customization.
try:
    button_actions_path = os.path.join(DATA_DIR, "button-actions.json")
    with open(button_actions_path, "r", encoding="utf-8") as f:
        button_actions_doc = json.load(f)
    if button_actions_doc.get("xiaomi_tv") == "Info":
        button_actions_doc["xiaomi_tv"] = "RunScript(script.kodiflix.info)"
        with open(button_actions_path, "w", encoding="utf-8") as f:
            json.dump(button_actions_doc, f, indent=2)
            f.write("\n")
        xbmc.log(
            "[VoiceHelper] migrated Xiaomi TV+ button directly to KodiFlix Info",
            xbmc.LOGINFO
        )
except Exception as e:
    xbmc.log(
        "[VoiceHelper] Xiaomi TV+ action migration failed: " + repr(e),
        xbmc.LOGWARNING
    )

# 1.0.5 migration: remote-map is a reusable protocol/button profile only.
# Strip physical identities left behind by pre-portable builds.
try:
    remote_map_path = os.path.join(DATA_DIR, "remote-map.json")
    with open(remote_map_path, "r", encoding="utf-8") as f:
        remote_map_doc = json.load(f)
    dev = remote_map_doc.setdefault("device", {})
    changed = False
    for key in ("address", "adapter"):
        if key in dev:
            dev.pop(key, None)
            changed = True
    if changed:
        remote_map_doc["format"] = max(int(remote_map_doc.get("format", 1)), 2)
        with open(remote_map_path, "w", encoding="utf-8") as f:
            json.dump(remote_map_doc, f, indent=2)
            f.write("\n")
except Exception as e:
    xbmc.log("[VoiceHelper] remote-map identity migration failed: " + repr(e), xbmc.LOGWARNING)

monitor = xbmc.Monitor()
daemon = os.path.join(ADDON_DIR, "daemon.py")
proc = None

DAEMON_RESTART_BASE_SECONDS = 3.0
DAEMON_RESTART_MAX_SECONDS = 15.0
daemon_restart_at = 0.0
daemon_restart_failures = 0
daemon_started_at = 0.0


class VoiceOverlay(xbmcgui.WindowXMLDialog):
    def onInit(self):
        # WindowXMLDialog texture paths are resolved against the skin, not
        # reliably against the add-on root. The legacy overlay worked because
        # Python immediately replaced its mic texture with an absolute path.
        # Do that explicitly for every production asset.
        try:
            media = os.path.join(ADDON_DIR, "resources", "media")
            mic = self.getControl(100)
            listening = self.getControl(110)
            processing = self.getControl(111)

            mic.setImage(os.path.join(media, "mic-listening-polished.png"), False)
            listening.setImage(os.path.join(media, "status-listening.png"), False)
            processing.setImage(os.path.join(media, "status-processing.png"), False)

            mic.setVisible(True)
            listening.setVisible(False)
            processing.setVisible(False)
            xbmc.log("[VoiceHelper UI] overlay initialized with absolute asset paths", xbmc.LOGINFO)
        except Exception as e:
            xbmc.log("[VoiceHelper UI] init: " + repr(e), xbmc.LOGERROR)

    def set_state(self, state, text, logo_url="", command=""):
        try:
            mic = self.getControl(100)
            listening = self.getControl(110)
            processing = self.getControl(111)

            media = os.path.join(ADDON_DIR, "resources", "media")
            if state == "listening":
                mic.setImage(os.path.join(media, "mic-listening-polished.png"), False)
            elif state in ("processing", "resolving"):
                mic.setImage(os.path.join(media, "mic-processing-polished.png"), False)

            mic.setVisible(True)
            listening.setVisible(state == "listening")
            processing.setVisible(state in ("processing", "resolving"))
            xbmc.log("[VoiceHelper UI] state=" + str(state), xbmc.LOGDEBUG)
        except Exception as e:
            xbmc.log("[VoiceHelper UI] state update: " + repr(e), xbmc.LOGERROR)



def normalize_remote_mac(value):
    raw = "".join(
        ch for ch in str(value or "").upper()
        if ch in "0123456789ABCDEF"
    )
    if len(raw) != 12:
        return ""
    return ":".join(raw[i:i+2] for i in range(0, 12, 2))


def configured_remote_mac():
    try:
        return normalize_remote_mac(
            xbmcaddon.Addon("service.voicehelper").getSetting("remote_mac")
        )
    except Exception:
        return ""


def migrate_legacy_remote_setting():
    """Import legacy remote.json once, then use Kodi settings as source of truth."""
    try:
        addon = xbmcaddon.Addon("service.voicehelper")
        current = normalize_remote_mac(addon.getSetting("remote_mac"))
        if current:
            if addon.getSetting("remote_mac") != current:
                addon.setSetting("remote_mac", current)
            return current

        if os.path.exists(REMOTE_FILE):
            with open(REMOTE_FILE, "r", encoding="utf-8") as f:
                legacy_doc = json.load(f)
            legacy = normalize_remote_mac(legacy_doc.get("remote_mac"))
            if legacy:
                addon.setSetting("remote_mac", legacy)
                xbmc.log(
                    f"[VoiceHelper] migrated legacy remote MAC {legacy} into addon settings",
                    xbmc.LOGINFO
                )
                return legacy
    except Exception as exc:
        xbmc.log(
            "[VoiceHelper] legacy remote migration failed: " + repr(exc),
            xbmc.LOGWARNING
        )
    return ""



class VoiceHelperConfigHandler(BaseHTTPRequestHandler):
    server_version = "KodiFlixVoiceHelper/1.0"

    def log_message(self, fmt, *args):
        xbmc.log(
            "[VoiceHelper Web] " + (fmt % args),
            xbmc.LOGDEBUG
        )

    def _send_html(self, message=""):
        addon = xbmcaddon.Addon("service.voicehelper")
        current = html.escape(addon.getSetting("remote_mac") or "")
        message = html.escape(message)

        body = f"""<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>KodiFlix VoiceHelper</title>
<style>
body{{font-family:Arial,sans-serif;background:#111;color:#eee;max-width:560px;margin:40px auto;padding:18px}}
.card{{background:#1e1e1e;border-radius:12px;padding:22px}}
h2{{margin-top:0}}
label{{display:block;margin:12px 0 4px}}
input,button{{box-sizing:border-box;width:100%;padding:12px;font-size:18px;border-radius:7px}}
input{{background:#2a2a2a;color:#fff;border:1px solid #555}}
button{{margin-top:12px;border:0;font-weight:bold;cursor:pointer}}
.small{{color:#aaa;font-size:14px}}
.msg{{margin-top:14px}}
</style>
</head>
<body>
<div class="card">
<h2>KodiFlix VoiceHelper</h2>
<form method="post" action="/">
<label>Remote Bluetooth MAC</label>
<input name="remote_mac" value="{current}" placeholder="AA:BB:CC:DD:EE:FF">
<button type="submit">Save</button>
</form>
<p class="small">Colons and dashes are optional.</p>
<p class="msg">{message}</p>
</div>
</body>
</html>"""
        payload = body.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(payload)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(payload)

    def do_GET(self):
        if self.path == "/health":
            payload = b"ok\n"
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.send_header("Content-Length", str(len(payload)))
            self.end_headers()
            self.wfile.write(payload)
            return
        self._send_html()

    def do_POST(self):
        try:
            length = int(self.headers.get("Content-Length", "0") or "0")
        except ValueError:
            length = 0

        raw = self.rfile.read(min(length, 4096)).decode(
            "utf-8", errors="replace"
        )
        form = parse_qs(raw)
        candidate = (form.get("remote_mac") or [""])[0]
        mac = normalize_remote_mac(candidate)

        if not mac:
            self._send_html("Invalid MAC: enter 12 hexadecimal digits.")
            return

        addon = xbmcaddon.Addon("service.voicehelper")
        addon.setSetting("remote_mac", mac)
        self._send_html("Saved " + mac)


def start_web_config():
    addon = xbmcaddon.Addon("service.voicehelper")

    enabled = (addon.getSetting("web_config_enabled") or "true").lower() == "true"
    if not enabled:
        xbmc.log("[VoiceHelper] web config disabled", xbmc.LOGINFO)
        return None

    try:
        port = int(addon.getSetting("web_config_port") or "8877")
    except ValueError:
        port = 8877

    port = max(1024, min(65535, port))

    try:
        server = ThreadingHTTPServer(
            ("0.0.0.0", port),
            VoiceHelperConfigHandler
        )
    except Exception as exc:
        xbmc.log(
            "[VoiceHelper] web config bind failed: " + repr(exc),
            xbmc.LOGERROR
        )
        return None

    thread = threading.Thread(
        target=server.serve_forever,
        name="voicehelper-web",
        daemon=True
    )
    thread.start()

    xbmc.log(
        f"[VoiceHelper] web config listening on 0.0.0.0:{port}",
        xbmc.LOGINFO
    )
    return server


def start_daemon():
    global proc, daemon_started_at

    remote_mac = configured_remote_mac()
    if not remote_mac:
        xbmc.log(
            "[VoiceHelper] daemon not started: Remote Bluetooth MAC is not configured",
            xbmc.LOGINFO
        )
        proc = None
        return False

    xbmc.log(
        f"[VoiceHelper] starting daemon for remote {remote_mac}",
        xbmc.LOGINFO
    )

    try:
        env = os.environ.copy()
        env["VOICEHELPER_REMOTE_MAC"] = remote_mac
        proc = subprocess.Popen(
            ["python3", "-u", daemon],
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
        )

        def _relay_daemon_output(child):
            try:
                for line in iter(child.stdout.readline, ""):
                    line = line.rstrip()
                    if line:
                        xbmc.log("[VoiceHelper daemon] " + line, xbmc.LOGINFO)
            except Exception as relay_exc:
                xbmc.log(
                    "[VoiceHelper] daemon output relay failed: " + repr(relay_exc),
                    xbmc.LOGWARNING,
                )

        threading.Thread(
            target=_relay_daemon_output,
            args=(proc,),
            name="VoiceHelper-daemon-log",
            daemon=True,
        ).start()
        daemon_started_at = time.monotonic()
        return True
    except Exception as e:
        proc = None
        xbmc.log(
            "[VoiceHelper] daemon launch failed: " + repr(e),
            xbmc.LOGERROR
        )
        return False


def video_is_playing():
    try:
        return xbmc.Player().isPlayingVideo()
    except Exception:
        return False


overlay = None
last_stamp = None
hide_at = 0.0
resolved_at = 0.0

# All web-config helpers are defined above this point before invocation.
migrate_legacy_remote_setting()
web_config_server = start_web_config()
last_remote_mac = configured_remote_mac()

try:
    # Fresh installs remain idle until a remote MAC is configured.
    if remote_is_configured() and not os.path.exists(SETUP_LOCK):
        start_daemon()
    else:
        if not remote_is_configured():
            xbmc.log(
                "[VoiceHelper] no remote configured; set Remote Bluetooth MAC in settings or web config",
                xbmc.LOGINFO
            )
        if os.path.exists(SETUP_LOCK):
            xbmc.log(
                "[VoiceHelper] setup lock present; daemon start deferred",
                xbmc.LOGINFO
            )

    while not monitor.abortRequested():
        current_remote_mac = configured_remote_mac()
        if current_remote_mac != last_remote_mac:
            xbmc.log(
                f"[VoiceHelper] remote MAC changed {last_remote_mac or '<unset>'} -> "
                f"{current_remote_mac or '<unset>'}",
                xbmc.LOGINFO
            )
            last_remote_mac = current_remote_mac

            if proc is not None and proc.poll() is None:
                proc.terminate()
                try:
                    proc.wait(timeout=3)
                except Exception:
                    proc.kill()
                proc = None

            daemon_restart_at = 0.0
            daemon_restart_failures = 0

        setup_active = os.path.exists(SETUP_LOCK)
        remote_configured = remote_is_configured()

        # Setup owns hci0 exclusively while scanning. Forgetting a remote also
        # retires the current daemon because there is no longer a valid target.
        should_pause = setup_active or not remote_configured
        if should_pause and proc is not None and proc.poll() is None:
            reason = "setup requested" if setup_active else "remote configuration removed"
            xbmc.log(
                f"[VoiceHelper] {reason}; stopping daemon",
                xbmc.LOGINFO
            )
            proc.terminate()
            try:
                proc.wait(timeout=3)
            except Exception:
                proc.kill()
            proc = None
            daemon_restart_at = 0.0
            daemon_restart_failures = 0

        # Process a genuine daemon exit. Do not schedule a death-dance when the
        # service intentionally retired it for setup/no-remote state.
        if proc is not None and proc.poll() is not None:
            rc = proc.returncode
            proc = None

            if (
                not monitor.abortRequested()
                and not setup_active
                and remote_configured
            ):
                if rc == 0:
                    # A resident daemon must not normally return while the
                    # service is active. Recover immediately and call it out.
                    delay = 0.25
                    xbmc.log(
                        "[VoiceHelper] daemon returned cleanly but unexpectedly; "
                        "resident HID/STT service must stay alive",
                        xbmc.LOGERROR,
                    )
                else:
                    daemon_restart_failures += 1
                    delay = min(
                        DAEMON_RESTART_MAX_SECONDS,
                        DAEMON_RESTART_BASE_SECONDS
                        * max(1, daemon_restart_failures)
                    )
                daemon_restart_at = time.monotonic() + delay

                xbmc.log(
                    f"[VoiceHelper] daemon exited rc={rc}; "
                    f"automatic recovery in {delay:.1f}s",
                    xbmc.LOGERROR
                )

                try:
                    with open(STATE_FILE, "w", encoding="utf-8") as f:
                        json.dump({
                            "state": "error",
                            "text": "Voice connection lost — reconnecting…",
                            "clearlogo": "",
                            "ts": time.time()
                        }, f)
                except Exception as e:
                    xbmc.log(
                        "[VoiceHelper] failed to publish recovery state: "
                        + repr(e),
                        xbmc.LOGERROR
                    )

        # Start immediately after Setup writes remote.json and releases hci0.
        if (
            proc is None
            and remote_configured
            and not setup_active
            and not daemon_restart_at
            and not monitor.abortRequested()
        ):
            start_daemon()

        # Genuine-failure recovery.
        if (
            proc is None
            and daemon_restart_at
            and time.monotonic() >= daemon_restart_at
            and not monitor.abortRequested()
            and remote_configured
            and not setup_active
        ):
            xbmc.log(
                "[VoiceHelper] attempting automatic daemon recovery",
                xbmc.LOGINFO
            )

            if start_daemon():
                daemon_restart_at = 0.0
            else:
                daemon_restart_failures += 1
                delay = min(
                    DAEMON_RESTART_MAX_SECONDS,
                    DAEMON_RESTART_BASE_SECONDS
                    * max(1, daemon_restart_failures)
                )
                daemon_restart_at = time.monotonic() + delay

        if (
            proc is not None
            and proc.poll() is None
            and daemon_restart_failures
            and daemon_started_at
            and (time.monotonic() - daemon_started_at) >= 30.0
        ):
            daemon_restart_failures = 0

        try:
            if os.path.exists(STATE_FILE):
                st = os.stat(STATE_FILE)
                if st.st_mtime != last_stamp:
                    last_stamp = st.st_mtime
                    with open(STATE_FILE, "r", encoding="utf-8") as f:
                        state = json.load(f)

                    name = state.get("state", "idle")
                    text = state.get("text", "")
                    logo_url = state.get("clearlogo", "")
                    command = state.get("command", "")

                    if name in ("idle", "result"):
                        if overlay:
                            overlay.close()
                            overlay = None
                        hide_at = 0.0
                        resolved_at = 0.0
                    else:
                        if overlay is None:
                            overlay = VoiceOverlay(
                                "VoiceOverlay.xml",
                                ADDON_DIR,
                                "Default",
                                "1080i"
                            )
                            overlay.show()

                        overlay.set_state(name, text, logo_url, command)

                        if name == "heard":
                            hide_at = time.monotonic() + 3.0
                        elif name == "resolving":
                            hide_at = time.monotonic() + 5.0
                        elif name == "resolved":
                            resolved_at = time.monotonic()
                            hide_at = resolved_at + 8.0
                        elif name == "error":
                            hide_at = time.monotonic() + 2.5
                        else:
                            hide_at = 0.0

        except Exception as e:
            xbmc.log("[VoiceHelper UI] state error: " + repr(e), xbmc.LOGWARNING)

        if (
            overlay
            and resolved_at
            and video_is_playing()
            and (time.monotonic() - resolved_at) >= 1.25
        ):
            overlay.close()
            overlay = None
            hide_at = 0.0
            resolved_at = 0.0

        if overlay and hide_at and time.monotonic() >= hide_at:
            overlay.close()
            overlay = None
            hide_at = 0.0
            resolved_at = 0.0

        if monitor.waitForAbort(0.10):
            break

finally:
    if web_config_server is not None:
        try:
            web_config_server.shutdown()
            web_config_server.server_close()
        except Exception:
            pass

    if overlay:
        try:
            overlay.close()
        except Exception:
            pass

    if proc is not None and proc.poll() is None:
        proc.terminate()
        try:
            proc.wait(timeout=3)
        except Exception:
            proc.kill()
