"""``mgf-cloud-provision`` — plan / apply / destroy a service spec.

    mgf-cloud-provision plan    spec.toml                 # read-only diff
    mgf-cloud-provision apply   spec.toml --yes           # converge (CREATE/REPLACE)
    mgf-cloud-provision destroy spec.toml --yes           # delete the service VM
    mgf-cloud-provision ci-host --dry-run                 # the Woodpecker CI host
    mgf-cloud-provision ci-host --with-agent --yes        # … + woodpecker-agent2

``ci-host`` (absorbed from ``mgf-fed provision-ci-host``) carries its spec +
bundle built-in — see :mod:`mgf.cloud_provision.ci_host`.

``--provider fake`` exercises the whole flow against mgf-cloud's in-memory
provider (no cloud, no credentials) — used by the tests + for dry inspection.
The real provider (``--provider infomaniak``) loads credentials via
``mgf.cloud.config.load`` from ``--clouds-yaml`` / the standard search paths.
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path
from typing import TYPE_CHECKING

from mgf.cloud.providers import get_provider

from mgf.cloud_provision import __version__, ci_host
from mgf.cloud_provision._provision import apply, destroy, plan
from mgf.cloud_provision._spec import load_spec
from mgf.cloud_provision.exceptions import ProvisionError, SpecError

if TYPE_CHECKING:
    from collections.abc import Sequence

    from mgf.cloud.providers.base import CloudProvider

_EXIT_OK = 0
_EXIT_PROVISION_ERR = 1
_EXIT_USAGE_ERR = 2


def _make_provider(provider: str, clouds_yaml: str | None, cloud: str | None) -> CloudProvider:
    if provider == "fake":
        return get_provider("fake")
    from mgf.cloud import config

    credentials = config.load(clouds_yaml=clouds_yaml, cloud=cloud or "openstack")
    return get_provider(provider, credentials)


def build_parser() -> argparse.ArgumentParser:
    # Shared flags live on a parent parser so they're accepted on either side of
    # the subcommand (`… apply spec --yes` works), mirroring mgf-cloud's CLI.
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--provider", default="infomaniak", help="provider name (or 'fake')")
    common.add_argument("--cloud", default=None, help="clouds.yaml cloud name")
    common.add_argument("--clouds-yaml", default=None, help="path to an OpenStack clouds.yaml")
    common.add_argument("--yes", action="store_true", help="confirm a mutating apply/destroy")
    common.add_argument(
        "--allow-replace", action="store_true", help="permit a destructive REPLACE on drift"
    )

    parser = argparse.ArgumentParser(
        prog="mgf-cloud-provision",
        description="Declarative provisioning of the federation's self-hosted services.",
    )
    parser.add_argument("--version", action="version", version=f"mgf-cloud-provision {__version__}")
    sub = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
    for name in ("plan", "apply", "destroy"):
        sp = sub.add_parser(name, parents=[common], help=f"{name} a service spec")
        sp.add_argument("spec", help="path to the service spec .toml")

    # ci-host — the built-in Woodpecker CI host spec (absorbed from mgf-fed
    # provision-ci-host). No positional spec: the bundle (current Postgres
    # compose + two-vhost Caddyfile) and the agent-VM spec ship in the package.
    ch = sub.add_parser(
        "ci-host",
        parents=[common],
        help="provision the Woodpecker CI host (built-in spec; --with-agent adds agent2)",
    )
    ch.add_argument("--hostname", default=ci_host.DEFAULT_HOSTNAME, help="host base name")
    ch.add_argument("--env", default=ci_host.DEFAULT_ENV, help="environment suffix")
    ch.add_argument("--flavor", default=None, help=f"VM flavour (default {ci_host.DEFAULT_FLAVOR})")
    ch.add_argument("--image", default=None, help="OS image (default Ubuntu 24.04 LTS)")
    ch.add_argument("--network", default=None, help="network (default ext-net1)")
    ch.add_argument(
        "--volume-size", type=int, default=40, help="boot-volume GB (0 = ephemeral disk)"
    )
    ch.add_argument(
        "--floating-ip",
        action="store_true",
        help="allocate a floating IP (only if NOT on ext-net1, which is direct-public)",
    )
    ch.add_argument("--key-name", default=None, help="existing SSH keypair to inject")
    ch.add_argument(
        "--bundle-dir",
        type=Path,
        default=None,
        help="override the packaged bundle (a dir with docker-compose.yml + Caddyfile)",
    )
    ch.add_argument(
        "--with-agent",
        action="store_true",
        help="also converge woodpecker-agent2 (the packaged agent-VM spec)",
    )
    ch.add_argument("--dry-run", action="store_true", help="print the plan; touch no cloud")
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    """Parse argv, run the subcommand, return a process exit code."""
    args = build_parser().parse_args(argv)
    try:
        if args.command == "ci-host":
            # A dry-run must stay credential-free, so the provider is resolved
            # inside provision_ci_host only when it actually applies.
            return ci_host.provision_ci_host(
                hostname=args.hostname,
                env=args.env,
                flavor=args.flavor,
                image=args.image,
                network=args.network,
                volume_size_gb=(args.volume_size or None),
                floating_ip=args.floating_ip,
                key_name=args.key_name,
                bundle_dir=args.bundle_dir,
                with_agent=args.with_agent,
                provider_name=args.provider,
                clouds_yaml=args.clouds_yaml,
                cloud=args.cloud,
                dry_run=args.dry_run,
                yes=args.yes,
                allow_replace=args.allow_replace,
            )

        spec = load_spec(args.spec)
        provider = _make_provider(args.provider, args.clouds_yaml, args.cloud)

        if args.command == "plan":
            decision = plan(spec, provider)
            print(f"{spec.name}: {decision.action.value.upper()} — {decision.reason}")
            return _EXIT_OK

        if not args.yes:
            decision = plan(spec, provider)
            print(
                f"{spec.name}: would {args.command} ({decision.action.value}); pass --yes to do it"
            )
            return _EXIT_OK

        if args.command == "apply":
            result = apply(spec, provider, allow_replace=args.allow_replace)
            ips = ", ".join(result.ip_addresses) or "(pending)"
            print(f"{result.name}: {result.action.value.upper()} id={result.server_id} ips={ips}")
            return _EXIT_OK

        # destroy
        removed = destroy(spec.name, provider)
        print(f"{spec.name}: {'DESTROYED' if removed else 'absent (nothing to destroy)'}")
        return _EXIT_OK
    except SpecError as exc:
        print(f"spec error: {exc}", file=sys.stderr)
        return _EXIT_USAGE_ERR
    except ProvisionError as exc:
        print(f"provision error: {exc}", file=sys.stderr)
        return _EXIT_PROVISION_ERR


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