#!/usr/bin/env python3

from __future__ import annotations

import argparse
import datetime as dt
import hashlib
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
PUBSPEC_PATH = ROOT / "pubspec.yaml"


def parse_pubspec_identity(pubspec_path: Path) -> tuple[str, str, str]:
    content = pubspec_path.read_text(encoding="utf-8")

    name_match = re.search(r"^name:\s*([^\s]+)\s*$", content, re.MULTILINE)
    version_match = re.search(r"^version:\s*([^\s]+)\s*$", content, re.MULTILINE)
    if not name_match or not version_match:
        raise RuntimeError("Failed to read app name or version from pubspec.yaml")

    package_name = name_match.group(1)
    version = version_match.group(1)
    if "+" in version:
        build_name, build_number = version.split("+", 1)
    else:
        build_name, build_number = version, "1"

    return package_name, build_name, build_number


def sha256_of(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def run_command(command: list[str]) -> None:
    print(f"$ {' '.join(command)}", flush=True)
    subprocess.run(command, cwd=ROOT, check=True)


def ensure_clean_dir(path: Path) -> None:
    if path.exists():
        shutil.rmtree(path)
    path.mkdir(parents=True, exist_ok=True)


def copy_file(src: Path, dest: Path) -> dict[str, object]:
    dest.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, dest)
    return {
        "path": str(dest.relative_to(ROOT)),
        "sizeBytes": dest.stat().st_size,
        "sha256": sha256_of(dest),
    }


def copy_tree(src: Path, dest: Path) -> None:
    ensure_clean_dir(dest)
    shutil.copytree(src, dest, dirs_exist_ok=True)


def append_common_flutter_args(command: list[str], args: argparse.Namespace) -> None:
    command.extend(["--release", f"--target={args.target}"])

    if args.flavor:
        command.extend(["--flavor", args.flavor])
    if args.build_name:
        command.append(f"--build-name={args.build_name}")
    if args.build_number:
        command.append(f"--build-number={args.build_number}")

    for item in args.dart_define:
        command.extend(["--dart-define", item])
    for item in args.dart_define_from_file:
        command.extend(["--dart-define-from-file", item])


def build_android(args: argparse.Namespace, manifest: dict[str, object]) -> None:
    android_symbols_dir = ROOT / "build" / "symbols" / "android"
    android_output_dir = args.output_dir / "android"

    appbundle_cmd = ["flutter", "build", "appbundle"]
    append_common_flutter_args(appbundle_cmd, args)
    appbundle_cmd.append(f"--split-debug-info={android_symbols_dir.relative_to(ROOT)}")
    run_command(appbundle_cmd)

    apk_cmd = [
        "flutter",
        "build",
        "apk",
        "--split-per-abi",
        "--target-platform",
        "android-arm,android-arm64,android-x64",
    ]
    append_common_flutter_args(apk_cmd, args)
    apk_cmd.append(f"--split-debug-info={android_symbols_dir.relative_to(ROOT)}")
    run_command(apk_cmd)

    google_play_dir = android_output_dir / "google-play"
    local_dir = android_output_dir / "local"
    testing_dir = android_output_dir / "testing"

    artifact_prefix = f"{args.package_name}-v{args.build_name}-b{args.build_number}"

    aab_src = ROOT / "build" / "app" / "outputs" / "bundle" / "release" / "app-release.aab"
    arm64_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-arm64-v8a-release.apk"
    armv7_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-armeabi-v7a-release.apk"
    x64_src = ROOT / "build" / "app" / "outputs" / "flutter-apk" / "app-x86_64-release.apk"

    artifacts = {
        "googlePlayAab": copy_file(aab_src, google_play_dir / f"{artifact_prefix}-google-play.aab"),
        "localArm64Apk": copy_file(arm64_src, local_dir / f"{artifact_prefix}-arm64-v8a.apk"),
        "localArmeabiV7aApk": copy_file(armv7_src, local_dir / f"{artifact_prefix}-armeabi-v7a.apk"),
        "testingX64Apk": copy_file(x64_src, testing_dir / f"{artifact_prefix}-x86_64-test.apk"),
    }

    if android_symbols_dir.exists():
        copy_tree(android_symbols_dir, google_play_dir / "symbols")
        artifacts["dartSymbolsDir"] = {
            "path": str((google_play_dir / "symbols").relative_to(ROOT)),
            "sizeBytes": sum(path.stat().st_size for path in (google_play_dir / "symbols").rglob("*") if path.is_file()),
        }

    manifest["android"] = artifacts


def build_ios(args: argparse.Namespace, manifest: dict[str, object]) -> None:
    ios_symbols_dir = ROOT / "build" / "symbols" / "ios"
    ios_output_dir = args.output_dir / "ios"

    command = ["flutter", "build", "ipa"]
    append_common_flutter_args(command, args)
    command.append(f"--split-debug-info={ios_symbols_dir.relative_to(ROOT)}")

    if args.ios_codesign:
        command.extend(["--export-method", args.ios_export_method])
        if args.ios_export_options_plist:
            command.append(f"--export-options-plist={args.ios_export_options_plist}")
    else:
        command.append("--no-codesign")

    run_command(command)

    artifact_prefix = f"{args.package_name}-v{args.build_name}-b{args.build_number}"
    artifacts: dict[str, object] = {}

    archive_src = ROOT / "build" / "ios" / "archive" / "Runner.xcarchive"
    ipa_candidates = sorted((ROOT / "build" / "ios" / "ipa").glob("*.ipa"))

    if archive_src.exists():
        copy_tree(archive_src, ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive")
        artifacts["archiveDir"] = {
            "path": str((ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive").relative_to(ROOT)),
            "sizeBytes": sum(path.stat().st_size for path in (ios_output_dir / "archive" / f"{artifact_prefix}.xcarchive").rglob("*") if path.is_file()),
        }

    if ipa_candidates:
        ipa_src = ipa_candidates[-1]
        artifacts["ipa"] = copy_file(ipa_src, ios_output_dir / "ipa" / f"{artifact_prefix}.ipa")

    if ios_symbols_dir.exists():
        copy_tree(ios_symbols_dir, ios_output_dir / "symbols")
        artifacts["dartSymbolsDir"] = {
            "path": str((ios_output_dir / "symbols").relative_to(ROOT)),
            "sizeBytes": sum(path.stat().st_size for path in (ios_output_dir / "symbols").rglob("*") if path.is_file()),
        }

    if not artifacts:
        raise RuntimeError("iOS build finished but no archive or ipa artifact was found.")

    manifest["ios"] = artifacts


def create_argument_parser() -> argparse.ArgumentParser:
    package_name, build_name, build_number = parse_pubspec_identity(PUBSPEC_PATH)
    timestamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
    default_output_dir = ROOT / "dist" / "release" / timestamp

    parser = argparse.ArgumentParser(
        description="Build release artifacts for Google Play, local Android distribution, and iOS."
    )
    parser.add_argument(
        "--platform",
        choices=["all", "android", "ios"],
        default="all",
        help="Select which platform artifacts to build.",
    )
    parser.add_argument("--target", default="lib/main.dart", help="Flutter entrypoint file.")
    parser.add_argument("--flavor", help="Optional Flutter flavor.")
    parser.add_argument("--build-name", default=build_name, help="Override version name.")
    parser.add_argument("--build-number", default=build_number, help="Override version code / CFBundleVersion.")
    parser.add_argument(
        "--output-dir",
        type=Path,
        default=default_output_dir,
        help="Directory used to store copied release artifacts.",
    )
    parser.add_argument(
        "--dart-define",
        action="append",
        default=[],
        help="Repeatable dart-define entries passed to flutter build.",
    )
    parser.add_argument(
        "--dart-define-from-file",
        action="append",
        default=[],
        help="Repeatable dart-define-from-file entries passed to flutter build.",
    )
    parser.add_argument(
        "--ios-codesign",
        action="store_true",
        help="Enable codesign for flutter build ipa. Default keeps the iOS build unsigned.",
    )
    parser.add_argument(
        "--ios-export-method",
        choices=["app-store", "ad-hoc", "development", "enterprise"],
        default="app-store",
        help="Export method used when --ios-codesign is enabled.",
    )
    parser.add_argument(
        "--ios-export-options-plist",
        help="Optional ExportOptions.plist used when --ios-codesign is enabled.",
    )
    parser.set_defaults(package_name=package_name)
    return parser


def main() -> int:
    parser = create_argument_parser()
    args = parser.parse_args()
    args.output_dir = args.output_dir.resolve()

    args.output_dir.mkdir(parents=True, exist_ok=True)

    manifest: dict[str, object] = {
        "generatedAt": dt.datetime.now().isoformat(timespec="seconds"),
        "packageName": args.package_name,
        "buildName": args.build_name,
        "buildNumber": args.build_number,
        "target": args.target,
        "flavor": args.flavor,
        "outputDir": str(args.output_dir.relative_to(ROOT)),
    }

    if args.platform in {"all", "android"}:
        build_android(args, manifest)
    if args.platform in {"all", "ios"}:
        build_ios(args, manifest)

    manifest_path = args.output_dir / "build_manifest.json"
    manifest_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

    print(f"Artifacts copied to: {args.output_dir}")
    print(f"Manifest written to: {manifest_path}")
    return 0


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