"""Command line interface for Mira."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import sys

from mira.builder.builder import MiraBuilder
from mira.crypto import decrypt_to_temp_zip, encrypt_zip, is_encrypted_mira, open_mira_zip
from mira.keys import resolve_secret
from mira.library.library import MiraLibrary
from mira.library.resolver import PackageResolver
from mira.manifest_schema import validate_manifest
from mira.signature import generate_keypair, verify_signature


def _resolve_key(args, label: str = "encryption key") -> str | None:
    return resolve_secret(
        getattr(args, "key", None),
        getattr(args, "keyfile", None),
        "MIRA_KEY",
        "MIRA_KEY_FILE",
        prompt=getattr(args, "prompt_key", False),
        label=label,
    )


def _resolve_sign_key(args) -> str | None:
    return resolve_secret(
        getattr(args, "sign_key", None),
        getattr(args, "sign_keyfile", None),
        "MIRA_SIGNING_KEY",
        "MIRA_SIGNING_KEY_FILE",
        prompt=getattr(args, "prompt_sign_key", False),
        label="signing key",
    )


def _resolve_verify_key(args) -> str | None:
    return resolve_secret(
        getattr(args, "verify_key", None),
        getattr(args, "verify_keyfile", None),
        "MIRA_VERIFY_KEY",
        "MIRA_VERIFY_KEY_FILE",
        prompt=getattr(args, "prompt_verify_key", False),
        label="verify key",
    )


def _hash_stream(handle) -> str:
    digest = hashlib.sha256()
    while True:
        chunk = handle.read(64 * 1024)
        if not chunk:
            break
        digest.update(chunk)
    return digest.hexdigest()


def _verify_file_hashes(zip_file, manifest: dict) -> None:
    file_hashes = manifest.get("file_hashes")
    if not isinstance(file_hashes, dict):
        raise RuntimeError("Manifest does not contain file_hashes")

    for path, expected in file_hashes.items():
        if not isinstance(path, str) or not isinstance(expected, str):
            raise RuntimeError("Invalid file_hashes entry")
        try:
            with zip_file.open(path) as handle:
                actual = _hash_stream(handle)
        except KeyError as exc:
            raise RuntimeError(f"Missing file in archive: {path}") from exc
        if actual != expected:
            raise RuntimeError(f"File hash mismatch: {path}")


def main(argv: list[str] | None = None) -> int:
    """Entry point for the Mira CLI."""
    parser = argparse.ArgumentParser(prog="mira", description="Mira package tool")
    subparsers = parser.add_subparsers(dest="command", required=True)

    build_parser = subparsers.add_parser("build", help="Build a project into a .mira")
    build_parser.add_argument("project_dir", nargs="?", default=".")
    build_parser.add_argument("-o", "--output", default="dist")
    build_parser.add_argument("--key", help="Encryption key for protected archives")
    build_parser.add_argument("--keyfile", help="Path to a file containing the key")
    build_parser.add_argument("--prompt-key", action="store_true")
    build_parser.add_argument("--cipher", help="Encryption cipher (aes-256-gcm or hmac-sha256-stream)")
    build_parser.add_argument("--sign-key", help="PEM-encoded private key for signing")
    build_parser.add_argument("--sign-keyfile", help="Path to a private key file")
    build_parser.add_argument("--prompt-sign-key", action="store_true")
    build_parser.add_argument("--embed-public-key", action="store_true")

    install_parser = subparsers.add_parser("install", help="Install a .mira package")
    install_parser.add_argument("mira_path")
    install_parser.add_argument("--key", help="Encryption key for protected archives")
    install_parser.add_argument("--keyfile", help="Path to a file containing the key")
    install_parser.add_argument("--prompt-key", action="store_true")

    uninstall_parser = subparsers.add_parser("uninstall", help="Uninstall a package")
    uninstall_parser.add_argument("name")
    uninstall_parser.add_argument("--key", help="Encryption key for protected archives")
    uninstall_parser.add_argument("--keyfile", help="Path to a file containing the key")
    uninstall_parser.add_argument("--prompt-key", action="store_true")

    list_parser = subparsers.add_parser("list", help="List installed packages")
    list_parser.add_argument("--key", help="Encryption key for protected archives")
    list_parser.add_argument("--keyfile", help="Path to a file containing the key")
    list_parser.add_argument("--prompt-key", action="store_true")

    info_parser = subparsers.add_parser("info", help="Show package info")
    info_parser.add_argument("name")
    info_parser.add_argument("--key", help="Encryption key for protected archives")
    info_parser.add_argument("--keyfile", help="Path to a file containing the key")
    info_parser.add_argument("--prompt-key", action="store_true")

    inspect_parser = subparsers.add_parser(
        "inspect", help="Inspect a .mira archive without installing"
    )
    inspect_parser.add_argument("mira_path")
    inspect_parser.add_argument("--key", help="Encryption key for protected archives")
    inspect_parser.add_argument("--keyfile", help="Path to a file containing the key")
    inspect_parser.add_argument("--prompt-key", action="store_true")

    view_parser = subparsers.add_parser("view", help="View package summary")
    view_parser.add_argument("mira_path")
    view_parser.add_argument("--key", help="Encryption key for protected archives")
    view_parser.add_argument("--keyfile", help="Path to a file containing the key")
    view_parser.add_argument("--prompt-key", action="store_true")
    view_parser.add_argument("--files", action="store_true", help="List file paths")

    verify_parser = subparsers.add_parser("verify", help="Verify package signature")
    verify_parser.add_argument("mira_path")
    verify_parser.add_argument("--key", help="Encryption key for protected archives")
    verify_parser.add_argument("--keyfile", help="Path to a file containing the key")
    verify_parser.add_argument("--prompt-key", action="store_true")
    verify_parser.add_argument("--verify-key", help="PEM-encoded public key")
    verify_parser.add_argument("--verify-keyfile", help="Path to a public key file")
    verify_parser.add_argument("--prompt-verify-key", action="store_true")
    verify_parser.add_argument("--skip-hash", action="store_true")

    run_parser = subparsers.add_parser("run", help="Run a .mira application")
    run_parser.add_argument("mira_path")
    run_parser.add_argument("--key", help="Encryption key for protected archives")
    run_parser.add_argument("--keyfile", help="Path to a file containing the key")
    run_parser.add_argument("--prompt-key", action="store_true")

    keygen_parser = subparsers.add_parser("keygen", help="Generate an Ed25519 keypair")
    keygen_parser.add_argument("--out-private", default="mira_private.pem")
    keygen_parser.add_argument("--out-public", default="mira_public.pem")
    keygen_parser.add_argument("--overwrite", action="store_true")

    reencrypt_parser = subparsers.add_parser(
        "reencrypt", help="Re-encrypt a .mira archive with a new key"
    )
    reencrypt_parser.add_argument("mira_path")
    reencrypt_parser.add_argument("--out", help="Output .mira path")
    reencrypt_parser.add_argument("--old-key", help="Old encryption key")
    reencrypt_parser.add_argument("--old-keyfile", help="Path to old key file")
    reencrypt_parser.add_argument("--prompt-old-key", action="store_true")
    reencrypt_parser.add_argument("--new-key", help="New encryption key")
    reencrypt_parser.add_argument("--new-keyfile", help="Path to new key file")
    reencrypt_parser.add_argument("--prompt-new-key", action="store_true")
    reencrypt_parser.add_argument("--cipher", help="Encryption cipher (aes-256-gcm or hmac-sha256-stream)")

    args = parser.parse_args(argv)
    library = MiraLibrary()

    try:
        if args.command == "build":
            key = _resolve_key(args)
            sign_key = _resolve_sign_key(args)
            builder = MiraBuilder(args.project_dir, output_dir=args.output)
            archive_path = builder.build(
                encryption_key=key,
                signing_key=sign_key,
                embed_public_key=args.embed_public_key,
                cipher=args.cipher,
            )
            print(archive_path)
            return 0

        if args.command == "install":
            key = _resolve_key(args)
            library.install(args.mira_path, key=key)
            return 0

        if args.command == "uninstall":
            key = _resolve_key(args)
            library.uninstall(args.name, key=key)
            return 0

        if args.command == "list":
            key = _resolve_key(args)
            packages = library.list(key=key)
            if not packages:
                print("No packages installed")
                return 0
            for manifest in packages:
                name = manifest.get("name", "unknown")
                version = manifest.get("version", "unknown")
                print(f"{name} {version}")
            return 0

        if args.command == "info":
            key = _resolve_key(args)
            manifest = library.info(args.name, key=key)
            print(json.dumps(manifest, indent=2, ensure_ascii=False))
            return 0

        if args.command == "inspect":
            resolver = PackageResolver()
            key = _resolve_key(args)
            manifest = resolver.read_manifest(args.mira_path, key=key)
            print(json.dumps(manifest, indent=2, ensure_ascii=False))
            print("\nContents:")
            with open_mira_zip(args.mira_path, key) as zip_file:
                for name in zip_file.namelist():
                    print(name)
            return 0

        if args.command == "view":
            resolver = PackageResolver()
            key = _resolve_key(args)
            manifest = resolver.read_manifest(args.mira_path, key=key)
            validate_manifest(manifest)
            encrypted = is_encrypted_mira(args.mira_path)

            print(f"Name: {manifest.get('name')}")
            print(f"Version: {manifest.get('version')}")
            print(f"Description: {manifest.get('description')}")
            print(
                f"Python: {manifest.get('python_version')} ({manifest.get('python_implementation')})"
            )
            print(
                f"Platform: {manifest.get('os')} {manifest.get('arch')} ({manifest.get('pointer_size')} bit)"
            )
            print(f"Include: {manifest.get('include')}")
            print(f"Entry point: {manifest.get('entry_point')}")
            print(f"Encrypted: {'yes' if encrypted else 'no'}")

            deps = manifest.get("dependencies") or {}
            print(f"Dependencies: {len(deps)}")
            for name, version in deps.items():
                print(f"  {name} {version}")

            files = manifest.get("files") or {}
            dlls = files.get("dlls", [])
            libs = files.get("libs", [])
            project_files = files.get("project", [])
            print(
                f"Files: project={len(project_files)}, libs={len(libs)}, dlls={len(dlls)}"
            )

            if args.files:
                print("\nProject files:")
                for path in project_files:
                    print(f"  {path}")
                print("\nLibs:")
                for path in libs:
                    print(f"  {path}")
                print("\nDLLs:")
                for path in dlls:
                    print(f"  {path}")
            return 0

        if args.command == "verify":
            key = _resolve_key(args)
            public_key = _resolve_verify_key(args)

            with open_mira_zip(args.mira_path, key) as zip_file:
                try:
                    manifest = json.loads(
                        zip_file.read("manifest.json").decode("utf-8")
                    )
                except KeyError as exc:
                    raise RuntimeError("manifest.json not found in archive") from exc
                validate_manifest(manifest)

                try:
                    signature_data = json.loads(
                        zip_file.read("signature.json").decode("utf-8")
                    )
                except KeyError as exc:
                    raise RuntimeError("signature.json not found in archive") from exc

                verify_signature(manifest, signature_data, public_key_pem=public_key)
                if not args.skip_hash:
                    _verify_file_hashes(zip_file, manifest)

            print("Verification OK")
            return 0

        if args.command == "run":
            from mira.runtime.runtime import MiraRuntime

            key = _resolve_key(args)
            runtime = MiraRuntime()
            runtime.run(args.mira_path, key=key)
            return 0

        if args.command == "keygen":
            private_path = args.out_private
            public_path = args.out_public
            if not args.overwrite:
                if os.path.exists(private_path) or os.path.exists(public_path):
                    raise RuntimeError("Output key files already exist")

            private_pem, public_pem = generate_keypair()
            with open(private_path, "w", encoding="utf-8") as handle:
                handle.write(private_pem)
            with open(public_path, "w", encoding="utf-8") as handle:
                handle.write(public_pem)
            print(f"Private key: {private_path}")
            print(f"Public key: {public_path}")
            return 0

        if args.command == "reencrypt":
            old_key = resolve_secret(
                args.old_key,
                args.old_keyfile,
                "MIRA_KEY",
                "MIRA_KEY_FILE",
                prompt=args.prompt_old_key,
                label="old key",
            )
            new_key = resolve_secret(
                args.new_key,
                args.new_keyfile,
                "MIRA_NEW_KEY",
                "MIRA_NEW_KEY_FILE",
                prompt=args.prompt_new_key,
                label="new key",
            )
            if not new_key:
                raise RuntimeError("New encryption key is required")

            cipher = args.cipher or os.environ.get("MIRA_CIPHER") or "aes-256-gcm"

            source_path = args.mira_path
            temp_zip = None
            try:
                if is_encrypted_mira(source_path):
                    if not old_key:
                        raise RuntimeError("Old encryption key is required")
                    temp_zip = decrypt_to_temp_zip(source_path, old_key)
                    source_path = temp_zip

                output_path = args.out
                if not output_path:
                    base, _ = os.path.splitext(args.mira_path)
                    output_path = base + ".reencrypted.mira"

                encrypt_zip(source_path, output_path, new_key, cipher=cipher)
                print(output_path)
                return 0
            finally:
                if temp_zip and os.path.exists(temp_zip):
                    os.remove(temp_zip)

    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1

    return 0


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