"""Shared TV control module — JointSpace, UPnP, ADB, Home Assistant, throttle.

Consolidates all TV/ADB/HA control functions used by sandman.py, sandman_bot.py,
and devices_server.py.  Import and call directly:

    import tv_control
    tv_control.js_key("Pause")
    vol = tv_control.get_volume()
"""

import concurrent.futures
import json
import logging
import os
import re
import socket
import ssl
import subprocess
import time
import urllib.request

import requests
from requests.auth import HTTPDigestAuth
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

log = logging.getLogger("tv_control")

# ─── Constants ────────────────────────────────────────────────────────────────

TV_IP = "192.168.1.50"

JS_DEVICE_ID = "a1b2c3d4e5f6"
JS_AUTH_KEY = "8f075a1826341135dcd3a9dd2ed30a49c327d7e002399fbaefae28d8e1f9936f"

UPNP_PORT = 49153
UPNP_SVC = "urn:schemas-upnp-org:service:RenderingControl:1"
UPNP_ENVELOPE = (
    '<?xml version="1.0"?>'
    '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"'
    ' s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
    "<s:Body>{body}</s:Body></s:Envelope>"
)

ADB_TARGET_PORT = 5555

APP_NAMES = {
    "com.google.android.youtube.tv": "YouTube",
    "com.netflix.ninja": "Netflix",
    "com.amazon.amazonvideo.livingroom": "Prime Video",
    "org.droidtv.channels": "Live TV",
    "org.droidtv.playtv": "Live TV",
    "org.droidtv.contentexplorer": "Media Browser",
    "com.apple.atve.androidtv.appletv": "Apple TV+",
    "com.google.android.apps.tv.launcherx": "Home",
    "com.google.android.tvlauncher": "Home",
    "com.google.android.katniss": "Google TV",
    "org.droidtv.settings": "Settings",
    "com.disney.disneyplus": "Disney+",
    "tv.mewatch": "mewatch",
    "com.spotify.tv.android": "Spotify",
}

# ─── HA Supervisor Token ──────────────────────────────────────────────────────

SUPERVISOR_TOKEN = ""
try:
    with open("/data/.ssh/environment") as _f:
        for _line in _f:
            if _line.startswith("SUPERVISOR_TOKEN="):
                SUPERVISOR_TOKEN = _line.strip().split("=", 1)[1]
except Exception:
    pass

# ─── TV IP Cache ──────────────────────────────────────────────────────────────

TV_IP_CACHE = "/share/sandman_bot_tv_ip"


def _get_tv_ip() -> str:
    """Return the current TV IP, checking cache file if module-level TV_IP is default."""
    global TV_IP
    # Try cache file
    try:
        cached = open(TV_IP_CACHE).read().strip()
        if cached:
            TV_IP = cached
    except Exception:
        pass
    return TV_IP


# ─── Internal Auth ────────────────────────────────────────────────────────────

def _js_auth() -> HTTPDigestAuth:
    return HTTPDigestAuth(JS_DEVICE_ID, JS_AUTH_KEY)


def _js_base(ip: str = None) -> str:
    ip = ip or _get_tv_ip()
    return f"https://{ip}:1926/6"


def _adb_target(ip: str = None) -> str:
    ip = ip or _get_tv_ip()
    return f"{ip}:{ADB_TARGET_PORT}"


def _upnp_url(ip: str = None) -> str:
    ip = ip or _get_tv_ip()
    return f"http://{ip}:{UPNP_PORT}/upnp/control/RenderingControl1"


# ─── JointSpace Functions ────────────────────────────────────────────────────

def js_get(path: str, timeout: float = 5, ip: str = None) -> dict | None:
    """GET a JointSpace endpoint.  Returns parsed JSON or None."""
    ip = ip or _get_tv_ip()
    try:
        url = f"{_js_base(ip)}/{path.lstrip('/')}"
        r = requests.get(url, auth=_js_auth(), verify=False, timeout=timeout)
        r.raise_for_status()
        return r.json()
    except Exception as e:
        log.debug("js_get /%s failed: %s", path, e)
        return None


def js_post(path: str, data: dict, timeout: float = 5, ip: str = None) -> bool:
    """POST to a JointSpace endpoint.  Returns True on success."""
    ip = ip or _get_tv_ip()
    try:
        url = f"{_js_base(ip)}/{path.lstrip('/')}"
        r = requests.post(url, json=data, auth=_js_auth(), verify=False, timeout=timeout)
        return r.status_code < 400
    except Exception as e:
        log.debug("js_post /%s failed: %s", path, e)
        return False


def js_key(key: str, ip: str = None) -> bool:
    """Send a key press via JointSpace."""
    return js_post("input/key", {"key": key}, ip=ip)


# ─── UPnP Functions ──────────────────────────────────────────────────────────

def _upnp_request(action: str, body_inner: str, ip: str = None) -> str | None:
    """Send a UPnP SOAP request to RenderingControl.  Returns response body or None."""
    ip = ip or _get_tv_ip()
    url = _upnp_url(ip)
    body = UPNP_ENVELOPE.format(body=body_inner)
    headers = {
        "Content-Type": 'text/xml; charset="utf-8"',
        "SOAPAction": f'"{UPNP_SVC}#{action}"',
    }
    try:
        req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
        with urllib.request.urlopen(req, timeout=5) as resp:
            return resp.read().decode()
    except Exception as e:
        log.debug("UPnP %s failed: %s", action, e)
        return None


def get_volume(ip: str = None) -> int | None:
    """Get current volume via UPnP.  Returns int or None."""
    body = f'<u:GetVolume xmlns:u="{UPNP_SVC}"><InstanceID>0</InstanceID><Channel>Master</Channel></u:GetVolume>'
    resp = _upnp_request("GetVolume", body, ip=ip)
    if resp is None:
        return None
    m = re.search(r"<CurrentVolume>(\d+)</CurrentVolume>", resp)
    return int(m.group(1)) if m else None


def set_volume(vol: int, ip: str = None) -> bool:
    """Set volume via UPnP (0-100)."""
    vol = max(0, min(100, vol))
    body = (f'<u:SetVolume xmlns:u="{UPNP_SVC}">'
            f"<InstanceID>0</InstanceID><Channel>Master</Channel>"
            f"<DesiredVolume>{vol}</DesiredVolume></u:SetVolume>")
    return _upnp_request("SetVolume", body, ip=ip) is not None


def set_mute(muted: bool, ip: str = None) -> bool:
    """Set mute state via UPnP."""
    val = "1" if muted else "0"
    body = (f'<u:SetMute xmlns:u="{UPNP_SVC}">'
            f"<InstanceID>0</InstanceID><Channel>Master</Channel>"
            f"<DesiredMute>{val}</DesiredMute></u:SetMute>")
    return _upnp_request("SetMute", body, ip=ip) is not None


# ─── TV State ────────────────────────────────────────────────────────────────

def get_power_state(ip: str = None) -> str:
    """Return 'On', 'Standby', or 'Unknown'."""
    data = js_get("powerstate", ip=ip)
    if data:
        return data.get("powerstate", "Unknown").capitalize()
    return "Unknown"


def get_audio_state(ip: str = None) -> dict:
    """Return {'volume': int, 'muted': bool}."""
    data = js_get("audio/volume", ip=ip)
    if data:
        return {"volume": data.get("current", 0), "muted": data.get("muted", False)}
    return {"volume": 0, "muted": False}


# ─── SSDP Discovery ─────────────────────────────────────────────────────────

def discover_tv(timeout: float = 5.0) -> str | None:
    """Find the Philips TV via SSDP multicast, falling back to JointSpace probe."""
    # Method 1: SSDP
    try:
        ssdp_msg = (
            "M-SEARCH * HTTP/1.1\r\n"
            "HOST: 239.255.255.250:1900\r\n"
            'MAN: "ssdp:discover"\r\n'
            f"MX: {int(timeout)}\r\n"
            "ST: ssdp:all\r\n\r\n"
        )
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
        sock.settimeout(min(timeout, 3))
        sock.sendto(ssdp_msg.encode(), ("239.255.255.250", 1900))
        try:
            while True:
                data, addr = sock.recvfrom(4096)
                text = data.decode(errors="replace")
                if "Philips" in text or "PhilipsIntelSDK" in text or "android" in text.lower():
                    sock.close()
                    log.info("SSDP discovered TV at %s", addr[0])
                    return addr[0]
        except socket.timeout:
            pass
        finally:
            try:
                sock.close()
            except Exception:
                pass
    except Exception:
        pass

    # Method 2: JointSpace probe on common subnet IPs
    def probe(check_ip):
        try:
            ctx = ssl._create_unverified_context()
            urllib.request.urlopen(
                urllib.request.Request(f"https://{check_ip}:1926/6/system"),
                timeout=2, context=ctx,
            )
            return check_ip
        except Exception:
            return None

    candidates = [f"192.168.1.{i}" for i in range(2, 30)]
    with concurrent.futures.ThreadPoolExecutor(max_workers=15) as pool:
        futs = {pool.submit(probe, ip): ip for ip in candidates}
        for f in concurrent.futures.as_completed(futs):
            result = f.result()
            if result:
                log.info("Probe discovered TV at %s", result)
                return result
    return None


# ─── Ambilight ───────────────────────────────────────────────────────────────

def ambilight_toggle(ip: str = None):
    """Toggle ambilight via menu navigation (AmbilightOnOff -> up 15x -> Confirm -> Back)."""
    ip = ip or _get_tv_ip()
    try:
        s = requests.Session()
        s.auth = _js_auth()
        s.verify = False
        url = f"{_js_base(ip)}/input/key"
        s.post(url, json={"key": "AmbilightOnOff"}, timeout=3)
        time.sleep(0.15)
        for _ in range(15):
            s.post(url, json={"key": "CursorUp"}, timeout=3)
        time.sleep(0.05)
        s.post(url, json={"key": "Confirm"}, timeout=3)
        s.post(url, json={"key": "Back"}, timeout=3)
    except Exception as e:
        log.warning("Ambilight toggle failed: %s", e)


def ambilight_glitch(off_duration: float = 3.0, ip: str = None):
    """Toggle ambilight off, wait, toggle back on."""
    ambilight_toggle(ip=ip)
    time.sleep(off_duration)
    ambilight_toggle(ip=ip)


# ─── ADB Functions ───────────────────────────────────────────────────────────

def adb_connect(ip: str = None) -> bool:
    """Connect to TV via ADB.  Returns True on success."""
    target = _adb_target(ip)
    try:
        r = subprocess.run(["adb", "connect", target],
                           capture_output=True, text=True, timeout=10)
        return "connected" in r.stdout.lower() or "already" in r.stdout.lower()
    except Exception as e:
        log.debug("adb_connect failed: %s", e)
        return False


def adb_screenshot(output_path: str = "/tmp/tv_screen.png", ip: str = None) -> str | None:
    """Take a screenshot via ADB.  Returns local path on success, None on failure."""
    target = _adb_target(ip)
    try:
        adb_connect(ip)
        subprocess.run(
            ["adb", "-s", target, "shell", "screencap", "-p", "/sdcard/screen.png"],
            capture_output=True, timeout=15, check=True,
        )
        subprocess.run(
            ["adb", "-s", target, "pull", "/sdcard/screen.png", output_path],
            capture_output=True, timeout=15, check=True,
        )
        return output_path
    except Exception as e:
        log.warning("adb_screenshot failed: %s", e)
        return None


def adb_get_current_app(ip: str = None) -> tuple:
    """Get current foreground app, playback state, and extras via ADB.

    Returns (package_name, playback_state, extras_dict) where extras may
    contain 'title', 'artist', 'position_s'.
    Any element may be None on failure.
    """
    target = _adb_target(ip)
    try:
        extras = {}

        # Top activity
        r = subprocess.run(["adb", "-s", target, "shell",
                            "dumpsys", "activity", "activities"],
                           capture_output=True, text=True, timeout=8)
        pkg = None
        for line in r.stdout.splitlines():
            if "topResumedActivity" in line:
                m = re.search(r"(\S+)/\S+", line)
                if m:
                    pkg = m.group(1).split()[-1]
                break

        # Media session — playback state + metadata
        state = None
        r2 = subprocess.run(["adb", "-s", target, "shell",
                             "dumpsys", "media_session"],
                            capture_output=True, text=True, timeout=8)
        found_active = False
        for line in r2.stdout.splitlines():
            if "active=true" in line:
                found_active = True
            if found_active and "state=PlaybackState" in line:
                if "PLAYING" in line:
                    state = "PLAYING"
                elif "PAUSED" in line:
                    state = "PAUSED"
                if state:
                    pos_m = re.search(r"position=(\d+)", line)
                    upd_m = re.search(r"updated=(\d+)", line)
                    spd_m = re.search(r"speed=([\d.]+)", line)
                    if pos_m and upd_m:
                        pos_ms = int(pos_m.group(1))
                        updated = int(upd_m.group(1))
                        speed = float(spd_m.group(1)) if spd_m else 1.0
                        try:
                            up_r = subprocess.run(
                                ["adb", "-s", target, "shell", "cat /proc/uptime"],
                                capture_output=True, text=True, timeout=3)
                            uptime_ms = int(float(up_r.stdout.split()[0]) * 1000)
                            elapsed_ms = (uptime_ms - updated) * speed
                            real_pos_ms = pos_ms + elapsed_ms
                            if real_pos_ms > 0:
                                extras["position_s"] = int(real_pos_ms // 1000)
                        except Exception:
                            if pos_ms > 0:
                                extras["position_s"] = pos_ms // 1000
            if found_active and "metadata:" in line and "size=" in line:
                m = re.search(r"description=(.+?)(?:,\s*null)?$", line)
                if m:
                    desc = m.group(1).strip().rstrip(", null").rstrip(",")
                    parts = [p.strip() for p in desc.split(",")]
                    if parts:
                        extras["title"] = parts[0]
                    if len(parts) > 1:
                        extras["artist"] = parts[1]
                found_active = False

        return pkg, state, extras
    except Exception as e:
        log.warning("adb_get_current_app failed: %s", e)
        return None, None, None


def adb_launch_app(package: str, ip: str = None) -> bool:
    """Launch an app on the TV via ADB.  Returns True on success."""
    target = _adb_target(ip)
    launch_intents = {
        "com.netflix.ninja": "com.netflix.ninja/.MainActivity",
        "com.google.android.youtube.tv": "com.google.android.youtube.tv/com.google.android.apps.youtube.tv.activity.ShellActivity",
        "com.apple.atve.androidtv.appletv": "com.apple.atve.androidtv.appletv/.MainActivity",
        "com.disney.disneyplus": "com.disney.disneyplus/.ui.splash.LaunchActivity",
        "com.amazon.amazonvideo.livingroom": "com.amazon.amazonvideo.livingroom/com.amazon.ignition.IgnitionActivity",
        "org.droidtv.playtv": "org.droidtv.playtv/.PlayTvActivity",
    }
    try:
        adb_connect(ip)
        component = launch_intents.get(package)
        if component:
            subprocess.run(["adb", "-s", target, "shell",
                           f"am start -a android.intent.action.MAIN -n {component} --activity-clear-top"],
                           capture_output=True, timeout=10)
        else:
            r = subprocess.run(["adb", "-s", target, "shell",
                                f"monkey -p {package} -c android.intent.category.LEANBACK_LAUNCHER 1"],
                               capture_output=True, timeout=10)
            if r.returncode != 0:
                subprocess.run(["adb", "-s", target, "shell",
                                f"monkey -p {package} -c android.intent.category.LAUNCHER 1"],
                               capture_output=True, timeout=10)
        return True
    except Exception as e:
        log.warning("adb_launch_app failed: %s", e)
        return False


# ─── HA Smart Home ───────────────────────────────────────────────────────────

def ha_api(method: str, path: str, data: dict = None) -> dict | None:
    """Call HA Supervisor API.  Returns parsed JSON or None."""
    try:
        url = f"http://supervisor/core/api{path}"
        body = json.dumps(data).encode() if data else None
        req = urllib.request.Request(
            url, data=body,
            headers={
                "Authorization": f"Bearer {SUPERVISOR_TOKEN}",
                "Content-Type": "application/json",
            },
            method=method,
        )
        resp = urllib.request.urlopen(req, timeout=10)
        return json.loads(resp.read())
    except Exception as e:
        log.debug("ha_api %s %s failed: %s", method, path, e)
        return None


def ha_get_device_states(device_list: list) -> list:
    """Return [(entity_id, friendly_name, state), ...] for given entity IDs."""
    states = ha_api("GET", "/states")
    if not states:
        return []
    result = []
    for e in states:
        eid = e["entity_id"]
        if eid in device_list:
            name = e.get("attributes", {}).get("friendly_name", eid)
            result.append((eid, name, e["state"]))
    return result


def ha_switch_toggle(entity_id: str) -> bool:
    """Toggle a HA switch/light.  Returns True on success."""
    try:
        state_data = ha_api("GET", f"/states/{entity_id}")
        if not state_data:
            return False
        current = state_data.get("state", "off")
        domain = entity_id.split(".")[0]
        service = "turn_off" if current == "on" else "turn_on"
        ha_api("POST", f"/services/{domain}/{service}", {"entity_id": entity_id})
        return True
    except Exception as e:
        log.warning("ha_switch_toggle failed: %s", e)
        return False


def ha_switch_flicker(entity_id: str, duration: float):
    """Toggle a HA switch for `duration` seconds, then restore original state."""
    try:
        state_data = ha_api("GET", f"/states/{entity_id}")
        if not state_data:
            return
        current = state_data.get("state", "off")
        domain = entity_id.split(".")[0]
        toggle_svc = "turn_on" if current == "off" else "turn_off"
        ha_api("POST", f"/services/{domain}/{toggle_svc}", {"entity_id": entity_id})
        time.sleep(duration)
        restore_svc = "turn_off" if current == "off" else "turn_on"
        ha_api("POST", f"/services/{domain}/{restore_svc}", {"entity_id": entity_id})
    except Exception as e:
        log.warning("ha_switch_flicker failed: %s", e)


# ─── Throttle ────────────────────────────────────────────────────────────────

def throttle_apply(tv_ip: str = None, bandwidth_kbps: int = 5000) -> bool:
    """Apply tc HTB throttle targeting the TV IP.  Returns True on success."""
    tv_ip = tv_ip or _get_tv_ip()
    dev = "end0"
    try:
        # Remove existing rules first
        subprocess.run(["tc", "qdisc", "del", "dev", dev, "root"],
                       capture_output=True)
        if bandwidth_kbps <= 0:
            return True
        subprocess.run(["tc", "qdisc", "add", "dev", dev, "root", "handle", "1:",
                        "htb", "default", "10"],
                       capture_output=True, check=True)
        subprocess.run(["tc", "class", "add", "dev", dev, "parent", "1:", "classid",
                        "1:10", "htb", "rate", "1000mbit"],
                       capture_output=True, check=True)
        subprocess.run(["tc", "class", "add", "dev", dev, "parent", "1:", "classid",
                        "1:20", "htb", "rate", f"{bandwidth_kbps}kbit",
                        "ceil", f"{bandwidth_kbps}kbit"],
                       capture_output=True, check=True)
        subprocess.run(["tc", "filter", "add", "dev", dev, "parent", "1:", "protocol",
                        "ip", "prio", "1", "u32", "match", "ip", "dst",
                        f"{tv_ip}/32", "flowid", "1:20"],
                       capture_output=True, check=True)
        return True
    except Exception as e:
        log.warning("throttle_apply failed: %s", e)
        return False


def speaker_play_sound(sound_file: str, volume: float = 0.3) -> bool:
    """Play a sound file on the Xiaomi Sound Pro speaker via HA media_player."""
    try:
        ha_api("POST", "/services/media_player/volume_set", {
            "entity_id": "media_player.sound_pro_1264",
            "volume_level": volume
        })
        ha_api("POST", "/services/media_player/play_media", {
            "entity_id": "media_player.sound_pro_1264",
            "media_content_type": "music",
            "media_content_id": f"http://192.168.1.187:8888/sounds/{sound_file}"
        })
        return True
    except Exception as e:
        log.warning("speaker_play_sound failed: %s", e)
        return False


SPEAKER_SOUNDS = [
    "knock_2.mp3",
    "cough_single.mp3",
    "throat_final2.mp3",
    "tongue_click.mp3",
    "custom_20to22.mp3",
    "exhale_loud.mp3",
    "exhale_soft.mp3",
]


def throttle_remove() -> bool:
    """Remove all tc throttle rules.  Returns True on success."""
    dev = "end0"
    try:
        subprocess.run(["tc", "qdisc", "del", "dev", dev, "root"],
                       capture_output=True)
        return True
    except Exception as e:
        log.warning("throttle_remove failed: %s", e)
        return False
