#!/usr/bin/env python3
"""Remove legacy `acdl:*` tag keys from all tagged AWS resources (REQ-162, P3).

The Nova rebrand (v1.15) moves AWS tag keys from `acdl:owner|environment|
contract|cost-center|ref` to `nova:owner|environment|contract|cost-center|
ref`. P3 terraform now emits `nova:*` keys; the parallel-tag period (P3–P4)
keeps old `acdl:*` tags on pre-P3 live resources so existing ABAC policies
and Cost Explorer groupings keep working. Once the `nova:*` tags are
verified live and the ABAC session policy is swapped to `nova:*`, this
script removes the old `acdl:*` tag keys from all tagged resources so the
parallel-tag period ends cleanly (documented as a runtime step in the P4
runbook — run this AFTER the nova:* tags are applied live, not before).

Design:
  - **Dry-run by default.** Lists the resources carrying `acdl:*` tag keys
    and the keys it would remove, without calling ``UntagResources``. Pass
    ``--apply`` to execute.
  - **Idempotent.** Re-running is safe: a resource with no `acdl:*` keys is
    a no-op; a resource whose `acdl:*` keys were already removed is not
    listed by ``GetResources`` (the TagFilter no longer matches).
  - **Key-list logic is unit-tested** (see ``tests/test_untag_acdl_keys.py``);
    the AWS I/O is thin boto3 glue around ``acdl_keys_in()`` +
    ``keys_to_untag()``.

Usage:
  python3 scripts/untag_acdl_keys.py                  # dry-run (all acdl:* keys)
  python3 scripts/untag_acdl_keys.py --apply          # execute
  python3 scripts/untag_acdl_keys.py --region us-east-1 --apply
  python3 scripts/untag_acdl_keys.py --key acdl:owner --key acdl:ref --apply

This script does NOT need live AWS to be importable; the boto3 client is
constructed lazily inside ``run()`` so the module can be imported + the
key-list logic unit-tested without credentials.
"""

from __future__ import annotations

import argparse
import sys
from typing import Iterable, Optional

try:
    import boto3
except ImportError:  # pragma: no cover - boto3 is a test dep
    boto3 = None  # type: ignore


# The full legacy tag-key set (mirrors nova_tagging.py LEGACY_TAGS + acdl:ref).
DEFAULT_LEGACY_KEYS = (
    "acdl:owner",
    "acdl:contract",
    "acdl:environment",
    "acdl:cost-center",
    "acdl:ref",
)


# ---------------------------------------------------------------------------
# Key-list logic (pure, unit-tested)
# ---------------------------------------------------------------------------

def acdl_keys_in(tag_keys: Iterable[str], legacy_keys: Iterable[str] = DEFAULT_LEGACY_KEYS) -> list[str]:
    """Return the subset of ``tag_keys`` that are legacy ``acdl:*`` keys.

    Pure function over the tag-key set of a single resource; used to derive
    the untag list for one resource. Order is preserved (input order).

    Examples:
      >>> acdl_keys_in(["acdl:owner", "nova:owner", "Name", "acdl:cost-center"])
      ['acdl:owner', 'acdl:cost-center']
      >>> acdl_keys_in(["nova:owner", "nova:contract", "Name"])
      []
      >>> acdl_keys_in([])
      []
    """
    legacy_set = set(legacy_keys)
    return [k for k in tag_keys if k in legacy_set]


def keys_to_untag(resource: dict, legacy_keys: Iterable[str] = DEFAULT_LEGACY_KEYS) -> list[str]:
    """Extract the acdl:* keys to untag from a resourcegroupstaggingapi resource blob.

    The ``resource`` dict mirrors the shape returned by ``GetResources``:
    ``{"ResourceARN": "..., "Tags": [{"Key": "...", "Value": "..."}, ...]}``.
    Returns the list of legacy acdl:* keys present on that resource.
    """
    tags = resource.get("Tags", []) or []
    tag_keys = [t.get("Key") for t in tags if isinstance(t, dict) and t.get("Key")]
    return acdl_keys_in(tag_keys, legacy_keys=legacy_keys)


def list_tagged_resources(client, legacy_keys: Iterable[str] = DEFAULT_LEGACY_KEYS) -> list[dict]:
    """List all resources carrying any legacy ``acdl:*`` tag key.

    Uses ``resourcegroupstaggingapi:GetResources`` with a TagFilter per legacy
    key (the API filters are OR'd across the TagFilter list). Pagination
    handled via the built-in paginator. Returns a list of resource blobs
    ``{"ResourceARN": ..., "Tags": [...]}``.
    """
    tag_filters = [{"Key": k} for k in legacy_keys]
    resources: list[dict] = []
    paginator = client.get_paginator("get_resources")
    for page in paginator.paginate(TagFilters=tag_filters):
        for r in page.get("ResourceMappingList", []):
            resources.append(r)
    return resources


def untag_one(client, resource_arn: str, keys: list[str]) -> None:
    """Remove the given tag keys from a single resource."""
    client.untag_resources(ResourceARNList=[resource_arn], TagKeys=keys)


def run(
    legacy_keys: Iterable[str] = DEFAULT_LEGACY_KEYS,
    region: Optional[str] = None,
    apply: bool = False,
    client=None,
) -> dict:
    """Run the untag pass. Returns a summary dict.

    When ``apply`` is False (default, dry-run), no AWS mutations happen —
    the function lists resources carrying acdl:* keys and reports the keys
    it would remove. When ``apply`` is True, it calls ``UntagResources`` per
    resource.

    A pre-built boto3 resourcegroupstaggingapi ``client`` may be injected
    for testing.
    """
    if apply and client is None:
        if boto3 is None:
            raise RuntimeError("boto3 is required for --apply (live AWS)")
        client = boto3.client("resourcegroupstaggingapi", region_name=region) if region else boto3.client("resourcegroupstaggingapi")
    if client is None and apply:
        raise RuntimeError("boto3 resourcegroupstaggingapi client required for --apply")

    summary = {"listed": 0, "untagged": 0, "keys_removed": 0, "errors": 0, "plan": []}
    legacy_list = list(legacy_keys)

    resources = list_tagged_resources(client, legacy_keys=legacy_list) if apply else _dry_run_list(legacy_list, client)
    summary["listed"] = len(resources)

    for r in resources:
        arn = r.get("ResourceARN", "")
        keys = keys_to_untag(r, legacy_keys=legacy_list)
        if not keys:
            continue
        if not apply:
            summary["plan"].append({"arn": arn, "keys": keys, "action": "untag"})
            continue
        try:
            untag_one(client, arn, keys)
            summary["untagged"] += 1
            summary["keys_removed"] += len(keys)
        except Exception as e:  # pragma: no cover - AWS error path
            summary["errors"] += 1
            summary["plan"].append({"arn": arn, "keys": keys, "action": f"error: {e}"})
    return summary


def _dry_run_list(legacy_keys: list[str], client) -> list[dict]:
    """In dry-run, list resources if a client is available; else return []."""
    if client is None:
        return []
    return list_tagged_resources(client, legacy_keys=legacy_keys)


def main(argv: Optional[list[str]] = None) -> int:
    parser = argparse.ArgumentParser(
        description="Remove legacy acdl:* tag keys from all tagged AWS resources (REQ-162, P3)."
    )
    parser.add_argument("--key", action="append", default=None,
                        help="Legacy acdl:* key to remove (repeatable; default: all 5 acdl:* keys)")
    parser.add_argument("--region", default=None, help="AWS region (default: boto3 default)")
    parser.add_argument("--apply", action="store_true", help="Execute the untag pass (default: dry-run)")
    args = parser.parse_args(argv)

    legacy_keys = tuple(args.key) if args.key else DEFAULT_LEGACY_KEYS
    mode = "APPLY" if args.apply else "DRY-RUN"
    print(f"[untag_acdl_keys] {mode}: removing keys {list(legacy_keys)} (region={args.region or 'default'})")
    summary = run(
        legacy_keys=legacy_keys,
        region=args.region,
        apply=args.apply,
    )
    print(f"[untag_acdl_keys] listed={summary['listed']} untagged={summary['untagged']} "
          f"keys_removed={summary['keys_removed']} errors={summary['errors']}")
    if not args.apply and summary["listed"] == 0:
        print("[untag_acdl_keys] (dry-run with no live AWS client: 0 resources listed; "
              "key-list logic is unit-tested in tests/test_untag_acdl_keys.py)")
    return 0 if summary["errors"] == 0 else 1


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