#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import sqlite3
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
GRADLE_FILE = REPO_ROOT / "android" / "app" / "build.gradle"
JAVA_HOME = "/Applications/Android Studio.app/Contents/jbr/Contents/Home"
API_BASE_URL = "https://iwanttoheal.phenomrom.com"
GITEA_URL = "https://git.whoagland.com"
GITEA_OWNER = "phenom"
GITEA_REPO = "i-want-to-heal"
GITEA_TOKEN = "ed2db3fd54546e9658377d0551b3fc3961583f1d"
BRANCH = "main"
TRUENAS_PATH = Path("/mnt/usbssds/apps/iwanttoheal/app")
PLAYER_DATA_TABLES = [
    "encounter_loot_roll_items",
    "encounter_loot_rolls",
    "dungeon_runs",
    "character_pvp_stats",
    "character_boss_stats",
    "character_inventory",
    "character_talents",
    "character_ability_slots",
    "action_encounter_loot_rolls",
    "action_dungeon_runs",
    "action_character_inventory",
    "action_gear_items",
    "action_characters",
    "sessions",
    "characters",
    "accounts",
]


def run(args: list[str], *, cwd: Path, env: dict[str, str] | None = None) -> None:
    print(f"$ {' '.join(args)}", flush=True)
    subprocess.run(args, cwd=cwd, env=env, check=True)


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Build and publish an Android release.")
    parser.add_argument(
        "--reset-player-data",
        action="store_true",
        help="After release, back up the database and delete all accounts, characters, sessions, runs, loot, inventory, and PvP stats.",
    )
    parser.add_argument(
        "--reset-db",
        type=Path,
        help="SQLite game.db path to reset. Defaults to mounted TrueNAS data/game.db when present, otherwise local data/game.db.",
    )
    parser.add_argument(
        "--yes-reset",
        action="store_true",
        help="Skip the RESET confirmation when used with --reset-player-data.",
    )
    args = parser.parse_args(argv)
    if args.yes_reset and not args.reset_player_data:
        parser.error("--yes-reset requires --reset-player-data")
    return args


def prompt_version() -> str:
    current = GRADLE_FILE.read_text()
    match = re.search(r'versionName\s+"([^"]+)"', current)
    current_version = match.group(1) if match else ""
    suffix = f" [{current_version}]" if current_version else ""
    version = input(f"Version{suffix}: ").strip() or current_version
    if not re.fullmatch(r"\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?", version):
        raise SystemExit("Version must look like 1.1.2")
    return version


def update_gradle_version(version: str) -> None:
    text = GRADLE_FILE.read_text()
    code_match = re.search(r"versionCode\s+(\d+)", text)
    if not code_match:
        raise SystemExit(f"Could not find versionCode in {GRADLE_FILE}")

    next_code = int(code_match.group(1)) + 1
    text = re.sub(r"versionCode\s+\d+", f"versionCode {next_code}", text, count=1)
    text = re.sub(r'versionName\s+"[^"]+"', f'versionName "{version}"', text, count=1)
    GRADLE_FILE.write_text(text)
    print(f"Android version: {version}, versionCode: {next_code}")


def build_apk(version: str) -> Path:
    update_gradle_version(version)
    env = os.environ.copy()
    env["JAVA_HOME"] = JAVA_HOME
    env["PATH"] = f"{JAVA_HOME}/bin:{env.get('PATH', '')}"
    env["VITE_API_BASE_URL"] = API_BASE_URL

    run(["npm", "run", "android:sync"], cwd=REPO_ROOT, env=env)
    run(["./gradlew", "clean", "assembleDebug"], cwd=REPO_ROOT / "android", env=env)

    source = REPO_ROOT / "android" / "app" / "build" / "outputs" / "apk" / "debug" / "app-debug.apk"
    apk = REPO_ROOT / f"IWantToHeal-Thor-v{version}.apk"
    shutil.copy2(source, apk)
    print(f"APK: {apk}")
    return apk


def commit_and_push(version: str) -> None:
    run(["git", "add", "."], cwd=REPO_ROOT)
    run(["git", "commit", "-m", f"Android build v{version}"], cwd=REPO_ROOT)
    run(["git", "push", "origin", BRANCH], cwd=REPO_ROOT)


def request_json(method: str, path: str, token: str, data: dict | None = None) -> dict:
    body = json.dumps(data).encode("utf-8") if data is not None else None
    headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
    req = urllib.request.Request(GITEA_URL + path, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", "replace")
        raise SystemExit(f"Gitea {method} failed ({exc.code}): {detail}") from exc


def upload_asset(path: str, token: str, apk: Path) -> dict:
    boundary = "----iwanttoheal-release-boundary"
    body = (
        f"--{boundary}\r\n"
        f'Content-Disposition: form-data; name="attachment"; filename="{apk.name}"\r\n'
        "Content-Type: application/vnd.android.package-archive\r\n\r\n"
    ).encode("utf-8")
    body += apk.read_bytes()
    body += f"\r\n--{boundary}--\r\n".encode("utf-8")
    headers = {
        "Authorization": f"token {token}",
        "Content-Type": f"multipart/form-data; boundary={boundary}",
    }
    req = urllib.request.Request(GITEA_URL + path, data=body, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", "replace")
        raise SystemExit(f"Gitea asset upload failed ({exc.code}): {detail}") from exc


def create_gitea_release(version: str, apk: Path) -> None:
    token = GITEA_TOKEN.strip()
    if not token or token == "PASTE_YOUR_GITEA_TOKEN_HERE":
        raise SystemExit("Set GITEA_TOKEN near top of scripts/release_game_cli.py")

    repo_path = f"/api/v1/repos/{GITEA_OWNER}/{GITEA_REPO}"
    release = request_json(
        "POST",
        repo_path + "/releases",
        token,
        {
            "tag_name": f"v{version}",
            "target_commitish": BRANCH,
            "name": f"v{version}",
            "body": f"I Want to Heal Android build v{version}",
            "draft": False,
            "prerelease": False,
        },
    )
    release_id = release.get("id")
    if not release_id:
        raise SystemExit(f"Gitea release missing id: {release}")

    asset_name = urllib.parse.quote(apk.name)
    upload_asset(f"{repo_path}/releases/{release_id}/assets?name={asset_name}", token, apk)
    print(f"Gitea release uploaded: v{version}")


def update_truenas() -> None:
    if TRUENAS_PATH.exists():
        run(["git", "pull"], cwd=TRUENAS_PATH)
        print("Restart app in TrueNAS UI.")
        return

    print("TrueNAS path not mounted here. Run on TrueNAS:")
    print(f"cd {TRUENAS_PATH}")
    print("git pull")
    print("Then restart app in TrueNAS UI.")


def default_reset_database_path() -> Path:
    truenas_database = TRUENAS_PATH / "data" / "game.db"
    if truenas_database.exists():
        return truenas_database
    return REPO_ROOT / "data" / "game.db"


def table_exists(database: sqlite3.Connection, table: str) -> bool:
    return database.execute(
        "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
        (table,),
    ).fetchone() is not None


def count_rows(database: sqlite3.Connection, table: str) -> int:
    if not table_exists(database, table):
        return 0
    return int(database.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0])


def backup_database(database_path: Path) -> Path:
    backup_dir = database_path.parent / "backups"
    backup_dir.mkdir(parents=True, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    backup_path = backup_dir / f"{database_path.stem}-pre-player-reset-{timestamp}{database_path.suffix}"

    source = sqlite3.connect(database_path)
    try:
        destination = sqlite3.connect(backup_path)
        try:
            source.backup(destination)
        finally:
            destination.close()
    finally:
        source.close()

    return backup_path


def reset_player_data(database_path: Path, *, assume_yes: bool) -> None:
    database_path = database_path.expanduser().resolve()
    if not database_path.exists():
        raise SystemExit(f"Database not found: {database_path}")

    if not assume_yes:
        print(f"Reset target: {database_path}")
        confirmation = input("Type RESET to delete all accounts/characters/player data: ").strip()
        if confirmation != "RESET":
            raise SystemExit("Player data reset cancelled.")

    backup_path = backup_database(database_path)
    database = sqlite3.connect(database_path)
    try:
        before = {
            "accounts": count_rows(database, "accounts"),
            "characters": count_rows(database, "characters"),
            "action_characters": count_rows(database, "action_characters"),
            "dungeon_runs": count_rows(database, "dungeon_runs"),
            "encounter_loot_rolls": count_rows(database, "encounter_loot_rolls"),
        }
        database.execute("PRAGMA foreign_keys = ON")
        database.execute("BEGIN")
        try:
            for table in PLAYER_DATA_TABLES:
                if table_exists(database, table):
                    database.execute(f'DELETE FROM "{table}"')
            database.execute("COMMIT")
        except Exception:
            database.execute("ROLLBACK")
            raise

        after_accounts = count_rows(database, "accounts")
        after_characters = count_rows(database, "characters")
        if after_accounts or after_characters:
            raise SystemExit(
                f"Player data reset incomplete: {after_accounts} accounts, {after_characters} characters remain."
            )

        print(f"Database backup: {backup_path}")
        print(
            "Player data reset: "
            f"{before['accounts']} accounts, "
            f"{before['characters']} characters, "
            f"{before['action_characters']} action characters, "
            f"{before['dungeon_runs']} dungeon runs, "
            f"{before['encounter_loot_rolls']} loot rolls removed."
        )
    finally:
        database.close()


def main(argv: list[str] | None = None) -> int:
    args = parse_args(sys.argv[1:] if argv is None else argv)
    os.chdir(REPO_ROOT)
    version = prompt_version()
    apk = build_apk(version)
    commit_and_push(version)
    create_gitea_release(version, apk)
    update_truenas()
    if args.reset_player_data:
        reset_player_data(args.reset_db or default_reset_database_path(), assume_yes=args.yes_reset)
    print("Done.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
