#!/usr/bin/env python3
"""
WE-TV 部署工具：通过电视自带的 ADB 网络调试完成安装和设置，不刷机、不 root，所有改动可恢复。

思路参考开源项目 cleanyourtv（MIT，https://github.com/Gathin23/cleanyourtv）：
只使用电视提供给机主的开发者选项和安卓自带的停用开关，改动前先记录，随时可以一键还原。

用法（电脑和电视在同一局域网，电视已打开“USB 调试 / 网络调试”）：
  python3 wetv-deploy.py --host 192.168.1.50 probe                  查看电视信息和当前桌面
  python3 wetv-deploy.py --host 192.168.1.50 install --apk WE-TV.apk --home [--boot]
                                                                     安装、授权自动升级、设为桌面、开机自启
  python3 wetv-deploy.py --host 192.168.1.50 debloat --list lists/xxx.txt
                                                                     按清单停用应用（不卸载）
  python3 wetv-deploy.py --host 192.168.1.50 check [--fix]           固件升级后检查改动是否被还原
  python3 wetv-deploy.py --host 192.168.1.50 restore [--uninstall]   全部还原
  python3 wetv-deploy.py --host 192.168.1.50 close [--disable-adb]   断开连接，可顺便关闭网络调试

需要电脑上有 adb（Android SDK Platform-Tools），或用环境变量 ADB 指定路径。
恢复记录保存在 ~/.wetv-deploy/ 下，每台电视一个文件。
"""

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path

PACKAGE = "com.we.tv"
HOME_ALIAS = f"{PACKAGE}/{PACKAGE}.HomeLauncher"
CONFIGURE = f"am broadcast -f 0x20 -n {PACKAGE}/.system.DeployReceiver -a {PACKAGE}.action.CONFIGURE"
RECORD_DIR = Path.home() / ".wetv-deploy"
ADB_PORT = 5555
AUTH_WAIT_SECONDS = 60
BATCH_SIZE = 5

# 这些应用停用后可能导致无法开机、无法设置或无法切换信号源，默认不处理，需 --force
PROTECTED_PATTERNS = [
    r"^android$",
    r"^com\.android\.(settings|systemui|packageinstaller|providers\..*|shell|phone|bluetooth)$",
    r"^com\.android\.tv\.settings$",
    r"^com\.google\.android\.(packageinstaller|gms|gsf)$",
    r"setupwizard|provision|oobe",
    r"inputmethod|keyboard|\.ime$",
    r"tvinput|hdmi|\.tvplayer|livetv|\.dtv|atv\.|tvsource|signalsource",
    r"factory|factorytest",
    rf"^{re.escape(PACKAGE)}$",
]


class DeployError(Exception):
    pass


# ---------------- adb ----------------

def adb_path() -> str:
    """依次查找：环境变量 ADB、PATH、本脚本所在目录、当前目录、Android Studio 默认 SDK 目录。"""
    path = os.environ.get("ADB") or shutil.which("adb")
    if path:
        return path
    names = ("adb.exe", "adb") if os.name == "nt" else ("adb",)
    folders = [Path(__file__).resolve().parent, Path.cwd(), Path.home() / "Library/Android/sdk/platform-tools"]
    for folder in folders:
        for name in names:
            candidate = folder / name
            if candidate.is_file():
                return str(candidate)
    raise DeployError("找不到 adb，请把 adb 和本工具放在同一文件夹，或用环境变量 ADB 指定路径")


class Tv:
    def __init__(self, host: str):
        self.serial = host if ":" in host else f"{host}:{ADB_PORT}"
        self.adb = adb_path()

    def run(self, *args: str, timeout: int = 60, check: bool = True) -> str:
        result = subprocess.run(
            [self.adb, "-s", self.serial, *args],
            capture_output=True, text=True, timeout=timeout,
        )
        output = (result.stdout + result.stderr).strip()
        if check and result.returncode != 0:
            raise DeployError(f"adb {' '.join(args)} 失败：{output}")
        return output

    def shell(self, command: str, timeout: int = 60, check: bool = True) -> str:
        return self.run("shell", command, timeout=timeout, check=check)

    def connect(self) -> None:
        out = subprocess.run([self.adb, "connect", self.serial], capture_output=True, text=True, timeout=30)
        text = (out.stdout + out.stderr).strip()
        if "connected" not in text:
            raise DeployError(f"连接 {self.serial} 失败：{text}\n请确认电视已打开网络调试，且和电脑在同一局域网")
        deadline = time.time() + AUTH_WAIT_SECONDS
        prompted = False
        while time.time() < deadline:
            state = subprocess.run([self.adb, "-s", self.serial, "get-state"], capture_output=True, text=True).stdout.strip()
            if state == "device":
                return
            if not prompted:
                print("请在电视上选择“允许 USB 调试”（建议勾选“始终允许”）…")
                prompted = True
            time.sleep(2)
        raise DeployError("等待授权超时，请在电视上允许调试后重试")

    def prop(self, name: str) -> str:
        return self.shell(f"getprop {name}", check=False).strip()

    def sdk(self) -> int:
        return int(self.prop("ro.build.version.sdk") or 0)

    def packages(self, flag: str = "") -> set:
        out = self.shell(f"pm list packages {flag}".strip(), check=False)
        return {line.split(":", 1)[1].strip() for line in out.splitlines() if line.startswith("package:")}

    def current_home(self) -> str:
        out = self.shell(
            "cmd package resolve-activity --brief -a android.intent.action.MAIN -c android.intent.category.HOME",
            check=False,
        )
        lines = [l.strip() for l in out.splitlines() if "/" in l and " " not in l.strip()]
        return lines[-1] if lines else ""

    def home_candidates(self) -> list:
        out = self.shell(
            "cmd package query-activities --brief -a android.intent.action.MAIN -c android.intent.category.HOME",
            check=False,
        )
        return [l.strip() for l in out.splitlines() if "/" in l and " " not in l.strip()]

    def configure(self, **extras: bool) -> str:
        """让 WE-TV 自己修改桌面入口和开机自启（-f 0x20 使刚安装、未打开过的应用也能收到）。"""
        args = " ".join(f"--ez {k} {'true' if v else 'false'}" for k, v in extras.items())
        return self.shell(f"{CONFIGURE} {args}", check=False)

    def set_home(self, component: str) -> bool:
        out = self.shell(f"cmd package set-home-activity {component}", check=False)
        return "Success" in out or self.current_home() == component


# ---------------- 恢复记录 ----------------

class Record:
    def __init__(self, tv: Tv):
        serial = tv.prop("ro.serialno") or tv.serial.replace(":", "_")
        name = re.sub(r"[^\w.-]", "_", f"{tv.prop('ro.product.brand')}-{tv.prop('ro.product.model')}-{serial}")
        self.path = RECORD_DIR / f"{name}.json"
        if self.path.exists():
            self.data = json.loads(self.path.read_text(encoding="utf-8"))
        else:
            self.data = {
                "device": {
                    "brand": tv.prop("ro.product.brand"),
                    "model": tv.prop("ro.product.model"),
                    "android": tv.prop("ro.build.version.release"),
                    "serial": serial,
                },
                "created": datetime.now().isoformat(timespec="seconds"),
                "original_home": "",
                "home_set_by_us": False,
                "boot_set_by_us": False,
                "installed_by_us": False,
                "disabled_by_us": [],
                "appops": [],
            }

    def save(self) -> None:
        RECORD_DIR.mkdir(parents=True, exist_ok=True)
        self.path.write_text(json.dumps(self.data, ensure_ascii=False, indent=2), encoding="utf-8")

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value
        self.save()


# ---------------- 命令 ----------------

def cmd_probe(tv: Tv, _args) -> None:
    print(f"品牌型号：{tv.prop('ro.product.brand')} {tv.prop('ro.product.model')}")
    print(f"系统版本：Android {tv.prop('ro.build.version.release')}（API {tv.sdk()}）")
    print(f"当前桌面：{tv.current_home() or '未知'}")
    candidates = tv.home_candidates()
    if candidates:
        print("可作为桌面的应用：" + "，".join(candidates))
    installed = PACKAGE in tv.packages()
    print(f"WE-TV：{'已安装' if installed else '未安装'}")
    print(f"已停用的应用：{len(tv.packages('-d'))} 个")


def cmd_install(tv: Tv, args) -> None:
    record = Record(tv)
    apk = Path(args.apk)
    if not apk.exists():
        raise DeployError(f"找不到安装包 {apk}")
    already = PACKAGE in tv.packages()
    print(f"正在安装 {apk.name} …")
    tv.run("install", "-r", str(apk), timeout=300)
    if not already:
        record["installed_by_us"] = True

    # 预先授权：自动升级时不再弹“安装未知应用”设置页；安卓 10 以上开机自启需要悬浮窗权限
    for op in ("REQUEST_INSTALL_PACKAGES", "SYSTEM_ALERT_WINDOW"):
        out = tv.shell(f"appops set {PACKAGE} {op} allow", check=False)
        if "Error" in out or "Unknown" in out:
            print(f"  授权 {op} 失败（系统不支持，可忽略）：{out}")
        elif op not in record["appops"]:
            record["appops"] = record["appops"] + [op]
    print("已授权自动升级安装")

    if args.boot:
        if "boot_start=true" in tv.configure(boot_start=True):
            record["boot_set_by_us"] = True
            print("已开启开机自动启动")
        else:
            print("开启开机自启失败，请在 WE-TV 设置里手动打开")
    if args.home:
        make_home(tv, record)


def make_home(tv: Tv, record: Record) -> None:
    if tv.sdk() < 24:
        print("安卓 7.0 以下无法用命令设置默认桌面，请在 WE-TV 设置里开启“设为电视桌面”后按主页键选择")
        return
    original = tv.current_home()
    if original and not original.startswith(PACKAGE) and not record["original_home"]:
        record["original_home"] = original
    tv.configure(home=True)
    time.sleep(1)
    if tv.set_home(HOME_ALIAS):
        record["home_set_by_us"] = True
        print(f"已设为默认桌面（原桌面 {record['original_home'] or '未知'} 已记录，可恢复）")
        return
    # 设置失败则回滚，避免电视停在没有桌面的状态
    tv.configure(home=False)
    if record["original_home"]:
        tv.set_home(record["original_home"])
    raise DeployError("该电视不允许更换桌面（部分国内品牌限制），已回滚。可改用 WE-TV 设置里的“开机自动启动”")


def read_list(path: str) -> list:
    packages = []
    for line in Path(path).read_text(encoding="utf-8").splitlines():
        name = line.split("#", 1)[0].strip()
        if name:
            packages.append(name)
    return packages


def is_protected(package: str) -> bool:
    return any(re.search(p, package) for p in PROTECTED_PATTERNS)


def cmd_debloat(tv: Tv, args) -> None:
    record = Record(tv)
    wanted = read_list(args.list)
    installed = tv.packages()
    already_disabled = tv.packages("-d")
    home = tv.current_home().split("/")[0]
    ime = tv.shell("settings get secure default_input_method", check=False).split("/")[0]

    plan, skipped = [], []
    for pkg in wanted:
        if pkg not in installed:
            skipped.append((pkg, "未安装"))
        elif pkg in already_disabled:
            skipped.append((pkg, "原本已停用"))
        elif pkg == home and not home.startswith(PACKAGE):
            skipped.append((pkg, "是当前桌面，请先用 install --home 设好 WE-TV"))
        elif pkg == ime:
            skipped.append((pkg, "是当前输入法"))
        elif is_protected(pkg) and not args.force:
            skipped.append((pkg, "系统关键应用（如确认无误可加 --force）"))
        else:
            plan.append(pkg)

    for pkg, reason in skipped:
        print(f"  跳过 {pkg}：{reason}")
    if not plan:
        print("没有需要停用的应用")
        return
    print(f"将停用 {len(plan)} 个应用（不卸载，可用 restore 还原），每 {BATCH_SIZE} 个一批：")
    for start in range(0, len(plan), BATCH_SIZE):
        batch = plan[start:start + BATCH_SIZE]
        print("  " + "\n  ".join(batch))
        if not args.yes and input("停用这一批？[y/N] ").strip().lower() != "y":
            print("已停止，已处理的部分可用 restore 还原")
            return
        for pkg in batch:
            out = tv.shell(f"pm disable-user --user 0 {pkg}", check=False)
            if "disabled" in out:
                record["disabled_by_us"] = sorted(set(record["disabled_by_us"]) | {pkg})
                print(f"  已停用 {pkg}")
            else:
                print(f"  停用 {pkg} 失败：{out}")
    print(f"完成。恢复记录：{record.path}")


def cmd_check(tv: Tv, args) -> None:
    record = Record(tv)
    disabled = tv.packages("-d")
    installed = tv.packages()
    drift = [p for p in record["disabled_by_us"] if p in installed and p not in disabled]
    home_lost = record["home_set_by_us"] and not tv.current_home().startswith(PACKAGE)
    if not drift and not home_lost:
        print("一切正常，之前的设置都还在")
        return
    for pkg in drift:
        print(f"  被重新启用：{pkg}")
    if home_lost:
        print(f"  桌面已变回：{tv.current_home()}")
    if not args.fix:
        print("加 --fix 重新应用")
        return
    for pkg in drift:
        tv.shell(f"pm disable-user --user 0 {pkg}", check=False)
    if home_lost:
        make_home(tv, record)
    print("已重新应用")


def cmd_restore(tv: Tv, args) -> None:
    record = Record(tv)
    if not record.path.exists():
        print("没有这台电视的恢复记录，无需还原")
        return
    for pkg in record["disabled_by_us"]:
        out = tv.shell(f"pm enable {pkg}", check=False)
        print(f"  已恢复 {pkg}" if "enabled" in out else f"  恢复 {pkg} 失败：{out}")
    if record.data.get("boot_set_by_us"):
        tv.configure(boot_start=False)
    if record["home_set_by_us"]:
        tv.configure(home=False)
        original = record["original_home"]
        if original and tv.set_home(original):
            print(f"  桌面已还原为 {original}")
        else:
            print("  已取消 WE-TV 桌面，按主页键时系统会让你选择桌面")
    for op in record["appops"]:
        tv.shell(f"appops set {PACKAGE} {op} default", check=False)
    if args.uninstall and record["installed_by_us"]:
        tv.run("uninstall", PACKAGE, check=False)
        print("  已卸载 WE-TV")
    done = record.path.with_suffix(f".restored-{datetime.now():%Y%m%d%H%M%S}.json")
    record.path.rename(done)
    print(f"还原完成，记录已归档：{done}")


def cmd_close(tv: Tv, args) -> None:
    if args.disable_adb:
        tv.shell("settings put global adb_enabled 0", check=False)
        print("已关闭电视的调试开关")
    subprocess.run([tv.adb, "disconnect", tv.serial], capture_output=True)
    print("已断开。网络调试是明文协议，用完建议在电视设置里关闭")


def main() -> int:
    parser = argparse.ArgumentParser(description="WE-TV 部署工具（不刷机、可恢复）")
    parser.add_argument("--host", required=True, help="电视的局域网 IP，可带端口，如 192.168.1.50:5555")
    sub = parser.add_subparsers(dest="command", required=True)
    sub.add_parser("probe", help="查看电视信息")
    p = sub.add_parser("install", help="安装 WE-TV 并授权自动升级")
    p.add_argument("--apk", required=True)
    p.add_argument("--home", action="store_true", help="同时设为默认桌面")
    p.add_argument("--boot", action="store_true", help="同时开启开机自动启动")
    p = sub.add_parser("debloat", help="按清单停用应用（兼容 cleanyourtv 的 debloat.txt 格式）")
    p.add_argument("--list", required=True)
    p.add_argument("--yes", action="store_true", help="不逐批确认")
    p.add_argument("--force", action="store_true", help="允许停用系统关键应用")
    p = sub.add_parser("check", help="检查改动是否被固件升级还原")
    p.add_argument("--fix", action="store_true")
    p = sub.add_parser("restore", help="还原全部改动")
    p.add_argument("--uninstall", action="store_true", help="同时卸载 WE-TV（仅限由本工具安装的）")
    p = sub.add_parser("close", help="断开连接")
    p.add_argument("--disable-adb", action="store_true", help="同时关闭电视的调试开关")
    args = parser.parse_args()

    commands = {
        "probe": cmd_probe, "install": cmd_install, "debloat": cmd_debloat,
        "check": cmd_check, "restore": cmd_restore, "close": cmd_close,
    }
    try:
        tv = Tv(args.host)
        tv.connect()
        commands[args.command](tv, args)
    except DeployError as e:
        print(f"错误：{e}", file=sys.stderr)
        return 1
    except subprocess.TimeoutExpired as e:
        print(f"错误：命令超时 {e.cmd}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
