from typing import List, Dict, Any

import typer
import importlib
import traceback
import yaml

from plumbum import local, ProcessExecutionError
from rich.console import Console
from rich.progress import Progress
from rich.prompt import Prompt

from utils.config import *
from utils.tools import *

# --- Paths ---

console = Console()
app = typer.Typer()


# ======================= PATCHING =========================
class Patch:
    def __init__(self, name: str, module):
        self.name = name
        self.module = module
        self.applied = False
        self.priority = getattr(module, "priority", 0)
        try:
            self.config = module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text())
        except Exception as e:
            console.print(f"[red]Ошибка при загрузке конфигурации патча {name}: {e}")
            self.config = module.Config()

    def apply(self, conf: Dict[str, Any]) -> bool:
        try:
            self.applied = bool(self.module.apply(self.config, conf))
            return self.applied
        except Exception as e:
            console.print(f"[red]Ошибка в патче {self.name}: {e}")
            traceback.print_exc()
            return False


# ======================= INIT =========================
@app.command()
def init():
    """Создание директорий и скачивание инструментов"""
    ensure_dirs()
    conf = load_config(console)

    for f in PATCHES.glob("*.py"):
        if f.name.startswith("todo_") or f.name == "__init__.py":
            continue
        patch = Patch(f.stem, __import__(f"patches.{f.stem}", fromlist=[""]))
        json_string = patch.config.model_dump_json()
        (CONFIGS / f"{patch.name}.json").write_text(json_string)

    if not (TOOLS / "apktool.jar").exists():
        download(console, conf.tools.apktool_jar_url, TOOLS / "apktool.jar")

    if not (TOOLS / "apktool").exists():
        download(console, conf.tools.apktool_wrapper_url, TOOLS / "apktool")
        (TOOLS / "apktool").chmod(0o755)

    try:
        local["java"]["-version"]()
        console.print("[green]Java найдена")
    except ProcessExecutionError:
        console.print("[red]Java не установлена")
        raise typer.Exit(1)


# ======================= INFO =========================
@app.command()
def info(patch_name: str = ""):
    """Вывод информации о патче"""
    conf = load_config(console).model_dump()
    if patch_name:
        patch = Patch(patch_name, __import__(f"patches.{patch_name}", fromlist=[""]))
        console.print(f"[green]Информация о патче {patch.name}:")
        console.print(f"  [yellow]Приоритет: {patch.priority}")
        console.print(f"  [yellow]Описание: {patch.module.__doc__}")
    else:
        console.print("[cyan]Список патчей:")
        for f in PATCHES.glob("*.py"):
            if f.name.startswith("todo_") or f.name == "__init__.py":
                continue
            name = f.stem
            if conf["patches"].get(name, {}).get("enabled", True):
                console.print(f"  [yellow]{name}: [green]✔ enabled")
            else:
                console.print(f"  [yellow]{name}: [red]✘ disabled")


def select_apk() -> Path:
    apks = [f for f in ORIGINAL.glob("*.apk")]
    if not apks:
        console.print("[red]Нет apk-файлов в папке original")
        raise typer.Exit(1)

    if len(apks) == 1:
        console.print(f"[green]Выбран {apks[0].name}")
        return apks[0]

    options = {str(i): apk for i, apk in enumerate(apks, 1)}
    for k, v in options.items():
        console.print(f"{k}. {v.name}")

    choice = Prompt.ask("Выберите номер", choices=list(options.keys()))
    return options[choice]


def decompile(apk: Path):
    console.print("[yellow]Декомпиляция apk...")
    run(
        console,
        [
            "java",
            "-jar", str(TOOLS / "apktool.jar"),
            "d", "-f",
            "-o", str(DECOMPILED),
            str(apk),
        ]
    )


def compile(apk: Path, patches: List[Patch]):
    console.print("[yellow]Сборка apk...")

    with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
        meta = yaml.safe_load(f)
        version_info = meta.get("versionInfo", {})
        version_code = version_info.get("versionCode", 0)
        version_name = version_info.get("versionName", "unknown")

    filename_version = version_name.lower().replace(" ", "-").replace(".", "-")
    out_apk = MODIFIED / f"Anixarty-mod-v{filename_version}.apk"
    aligned = out_apk.with_stem(out_apk.stem + "-aligned")
    signed = out_apk.with_stem(out_apk.stem + "-mod")

    run(
        console,
        [
            "java",
            "-jar", str(TOOLS / "apktool.jar"),
            "b", str(DECOMPILED),
            "-o", str(out_apk),
        ]
    )
    run(
        console,
        ["zipalign", "-v", "4", str(out_apk), str(aligned)]
    )
    run(
        console,
        [
            "apksigner", "sign",
            "--v1-signing-enabled", "false",
            "--v2-signing-enabled", "true",
            "--v3-signing-enabled", "true",
            "--ks", "keystore.jks",
            "--ks-pass", "file:keystore.pass",
            "--out", str(signed),
            str(aligned),
        ]
    )

    console.print("[green]✔ APK успешно собран и подписан")

    with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
        f.write(f"Anixarty mod v {version_name} ({version_code})\n")
        for p in patches:
            f.write(f"{'✔' if p.applied else '✘'} {p.name}\n")


@app.command()
def build(
    force: bool = typer.Option(False, "--force", "-f", help="Принудительная сборка"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
):
    """Декомпиляция, патчи и сборка apk"""
    conf = load_config(console)
    apk = select_apk()
    decompile(apk)

    patch_objs: List[Patch] = []
    conf.base |= {"verbose": verbose}

    for f in PATCHES.glob("*.py"):
        if f.name.startswith("todo_") or f.name == "__init__.py":
            continue
        name = f.stem
        module = importlib.import_module(f"patches.{name}")
        if not module.Config.model_validate_json((CONFIGS / f"{name}.json").read_text()):
            console.print(f"[yellow]≫ Пропускаем {name}")
            continue
        patch_objs.append(Patch(name, module))

    patch_objs.sort(key=lambda p: p.priority, reverse=True)

    console.print("[cyan]Применение патчей")
    with Progress() as progress:
        task = progress.add_task("Патчи", total=len(patch_objs))
        for p in patch_objs:
            ok = p.apply(conf.base)
            progress.console.print(f"{'✔' if ok else '✘'} {p.name}")
            progress.advance(task)

    successes = sum(p.applied for p in patch_objs)
    if successes == len(patch_objs):
        compile(apk, patch_objs)
    elif successes > 0 and (
        force or Prompt.ask("Продолжить сборку?", choices=["y", "n"]) == "y"
    ):
        compile(apk, patch_objs)
    else:
        console.print("[red]Сборка отменена")
        raise typer.Exit(1)


if __name__ == "__main__":
    app()
