from pathlib import Path
from typing import List

import httpx
import typer
import importlib
import traceback
import yaml

from pydantic import BaseModel, ValidationError
from plumbum import local, ProcessExecutionError
from rich.console import Console
from rich.progress import Progress
from rich.prompt import Prompt
from rich.table import Table

# --- Paths ---
TOOLS = Path("tools")
ORIGINAL = Path("original")
MODIFIED = Path("modified")
DECOMPILED = Path("decompiled")
PATCHES = Path("patches")

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


# ======================= CONFIG =========================
class ToolsConfig(BaseModel):
    apktool_jar_url: str
    apktool_wrapper_url: str


class XmlNamespaces(BaseModel):
    android: str
    app: str


class BaseSection(BaseModel):
    tools: ToolsConfig
    xml_ns: XmlNamespaces


class Config(BaseModel):
    base: BaseSection
    patches: dict


def load_config() -> Config:
    try:
        return Config.model_validate_json(Path("config.json").read_text())
    except FileNotFoundError:
        console.print("[red]Файл config.json не найден")
        raise typer.Exit(1)
    except ValidationError as e:
        console.print("[red]Ошибка валидации config.json:", e)
        raise typer.Exit(1)


# ======================= UTILS =========================
def ensure_dirs():
    for d in [TOOLS, ORIGINAL, MODIFIED, DECOMPILED, PATCHES]:
        d.mkdir(exist_ok=True)


def run(cmd: List[str], hide_output=True):
    prog = local[cmd[0]][cmd[1:]]
    try:
        prog() if hide_output else prog & FG
    except ProcessExecutionError as e:
        console.print(f"[red]Ошибка при выполнении команды: {' '.join(cmd)}")
        console.print(e.stderr)
        raise typer.Exit(1)


def download(url: str, dest: Path):
    console.print(f"[cyan]Скачивание {url} → {dest.name}")

    with httpx.Client(follow_redirects=True, timeout=60.0) as client:
        with client.stream("GET", url) as response:
            response.raise_for_status()
            total = int(response.headers.get("Content-Length", 0))

            dest.parent.mkdir(parents=True, exist_ok=True)

            with open(dest, "wb") as f, Progress(console=console) as progress:
                task = progress.add_task("Загрузка", total=total if total else None)
                for chunk in response.iter_bytes(chunk_size=8192):
                    f.write(chunk)
                    progress.update(task, advance=len(chunk))


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

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

    if not (TOOLS / "apktool").exists():
        download(conf.base.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().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")


# ======================= PATCHING =========================
class Patch:
    def __init__(self, name: str, module):
        self.name = name
        self.module = module
        self.applied = False
        self.priority = getattr(module, "priority", 0)

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


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(
        [
            "java",
            "-jar",
            str(TOOLS / "apktool.jar"),
            "d",
            "-f",
            "-o",
            str(DECOMPILED),
            str(apk),
        ]
    )


def compile(apk: Path, patches: List[Patch]):
    console.print("[yellow]Сборка apk...")
    out_apk = MODIFIED / apk.name
    aligned = out_apk.with_stem(out_apk.stem + "-aligned")
    signed = out_apk.with_stem(out_apk.stem + "-mod")

    run(
        [
            "java",
            "-jar",
            str(TOOLS / "apktool.jar"),
            "b",
            str(DECOMPILED),
            "-o",
            str(out_apk),
        ]
    )
    run(["zipalign", "-v", "4", str(out_apk), str(aligned)])
    run(
        [
            "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),
        ]
    )

    with open(DECOMPILED / "apktool.yml", encoding="utf-8") as f:
        meta = yaml.safe_load(f)
        version_str = " ".join(
            f"{k}:{v}" for k, v in meta.get("versionInfo", {}).items()
        )

    with open(MODIFIED / "report.log", "w", encoding="utf-8") as f:
        f.write(f"anixart mod {version_str}\n")
        for p in patches:
            f.write(f"{p.name}: {'applied' if p.applied else 'failed'}\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().model_dump()
    apk = select_apk()
    decompile(apk)

    patch_settings = conf.get("patches", {})
    patch_objs: List[Patch] = []

    for f in PATCHES.glob("*.py"):
        if f.name.startswith("todo_") or f.name == "__init__.py":
            continue
        name = f.stem
        settings = patch_settings.get(name, {})
        if not settings.get("enabled", True):
            console.print(f"[yellow]≫ Пропускаем {name}")
            continue
        module = importlib.import_module(f"patches.{name}")
        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(patch_settings.get(p.name, {}) | conf.get("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()
