#!/usr/bin/env python3

import argparse
import http.client
import json
import logging
import os
import shutil
import socket
import ssl
import subprocess
import sys
import tempfile
from pathlib import Path

# Pre-Execution setup ---------------------------------------------------------

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

log = logging.getLogger(__name__)

ENFORCE_POLICIES = True
STACKS_DIR = "stacks"
STATE_BUCKET = "hca-iac-state-bucket"
PLAN_BUCKET = "hca-iac-plan-bucket"
LOG_BUCKET = "hca-iac-log-bucket"
LOG_ARCHIVE_ACCOUNT_ID = "STUB_ACCOUNT_ID"
LOG_ARCHIVE_ROLE = "hca-iac-execution-role"
JF_SERVER_ID = "hcassc-iac"
JF_DOMAIN = "hcassc.jfrog.io"
JF_PUB_RELEASES_REPO = "pub-releases-jfrog-remote"
JF_TF_PROVIDERS_MIRROR_URL = "https://hcassc.jfrog.io/artifactory/api/terraform/iac-tf-providers-virtual/providers/"

REQUIRED_TOOLS = [
    ("terraform", "version"),
    ("tflint", "--version"),
    ("opa", "version"),
    ("aws", "--version"),
    ("jf", "--version"),
]

REQUIRED_ENV_VARS = [
    "BITBUCKET_COMMIT",
    "AWS_ROLE_ARN",
    "AWS_REGION",
    "JFRW_TOKEN",
]

# Runtime overrides ------------------------------------------------------------

os.environ["JFROG_CLI_AVOID_NEW_VERSION_WARNING"] = "true"

# Helper functions -------------------------------------------------------------


def get_repo_root() -> Path:
    result: subprocess.CompletedProcess[str] = subprocess.run(
        ["git", "rev-parse", "--show-toplevel"],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        log.error("not a git repository or git not found")
        sys.exit(1)
    return Path(result.stdout.strip())


def check_aws() -> bool:
    try:
        result: subprocess.CompletedProcess[str] = subprocess.run(
            ["aws", "sts", "get-caller-identity"],
            capture_output=True,
            text=True,
            timeout=30,
        )
        if result.returncode == 0:
            identity = json.loads(result.stdout)
            log.info("aws auth ok - operating as %s", identity.get("Arn"))
            return True
        log.error("aws auth failed: %s", result.stderr.strip())
        return False
    except subprocess.TimeoutExpired:
        log.error("aws auth check timed out")
        return False
    except Exception:
        log.error("aws auth check failed: unexpected error")
        return False


def setup_jfrog() -> bool:
    # configure jf cli
    result: subprocess.CompletedProcess[str] = subprocess.run(
        [
            "jf",
            "config",
            "add",
            JF_SERVER_ID,
            "--url",
            f"https://{JF_DOMAIN}",
            "--access-token",
            os.environ["JFRW_TOKEN"],
            "--interactive=false",
            "--overwrite=true",
        ],
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        log.error("jfrog config failed: %s", result.stderr.strip())
        return False
    log.info("jfrog configured: %s", JF_SERVER_ID)

    # validate connectivity
    ping: subprocess.CompletedProcess[str] = subprocess.run(
        ["jf", "rt", "ping", "--server-id", JF_SERVER_ID],
        capture_output=True,
        text=True,
        timeout=10,
    )
    if ping.returncode != 0:
        log.error("jfrog ping failed: %s", ping.stderr.strip())
        return False
    log.info("jfrog connectivity ok")
    return True


def assume_log_archive_role(stack_name: str) -> dict[str, str] | None:
    role_arn = f"arn:aws:iam::{LOG_ARCHIVE_ACCOUNT_ID}:role/{LOG_ARCHIVE_ROLE}"
    session_name = f"hca-iac-log-{stack_name}"

    try:
        result: subprocess.CompletedProcess[str] = subprocess.run(
            [
                "aws",
                "sts",
                "assume-role",
                "--role-arn",
                role_arn,
                "--role-session-name",
                session_name,
            ],
            capture_output=True,
            text=True,
            timeout=30,
        )
        if result.returncode != 0:
            log.error("role assumption failed: %s", result.stderr.strip())
            return None

        creds = json.loads(result.stdout)["Credentials"]
        log.info("assumed log archive role: %s", session_name)
        return {
            "AWS_ACCESS_KEY_ID": creds["AccessKeyId"],
            "AWS_SECRET_ACCESS_KEY": creds["SecretAccessKey"],
            "AWS_SESSION_TOKEN": creds["SessionToken"],
        }
    except subprocess.TimeoutExpired:
        log.error("role assumption timed out")
        return None
    except Exception:
        log.error("role assumption failed: unexpected error")
        return None


def check_tfbackend(stack_dir: Path) -> bool:
    stack_name = stack_dir.name
    meta_path = stack_dir / ".terraform" / "terraform.tfstate"

    if not meta_path.exists():
        log.error("backend not configured for %s", stack_name)
        log.error('ensure stack defines empty [ backend "s3" {} ] in terraform block')
        return False

    meta = json.loads(meta_path.read_text())
    backend = meta.get("backend", {})
    backend_type = backend.get("type", "")

    if backend_type != "s3":
        log.error(
            "stack %s is not using s3 backend - found: %s",
            stack_name,
            backend_type or "local",
        )
        return False

    config = backend.get("config", {})
    bucket = config.get("bucket", "")
    key = config.get("key", "")
    region = config.get("region", "")
    encrypt = config.get("encrypt", False)
    use_lockfile = config.get("use_lockfile", False)
    state_uri = f"s3://{bucket}/{key}"

    log.info(
        "backend ok: %s [region: %s, encrypt: %s, lockfile: %s]",
        state_uri,
        region,
        encrypt,
        use_lockfile,
    )
    return True


def write_terraformrc() -> Path:
    content = (
        "provider_installation {\n"
        "  network_mirror {\n"
        f'    url = "{JF_TF_PROVIDERS_MIRROR_URL}"\n'
        "  }\n"
        "}\n"
    )
    rc_file = Path(tempfile.mkstemp(suffix=".terraformrc")[1])
    rc_file.write_text(content)
    log.info("terraformrc written: %s", rc_file)
    return rc_file


def get_changed_stacks(root: Path) -> list[Path]:
    is_pr = os.environ.get("BITBUCKET_PR_ID") is not None

    if is_pr:
        subprocess.run(
            ["git", "fetch", "origin", "main"],
            capture_output=True,
            cwd=root,
        )
        merge_base: str = subprocess.run(
            ["git", "merge-base", "origin/main", "HEAD"],
            capture_output=True,
            text=True,
            cwd=root,
        ).stdout.strip()
        cmd = ["git", "diff", "--name-only", merge_base, "HEAD"]
    else:
        cmd = ["git", "diff", "--name-only", "HEAD~1", "HEAD"]

    result: subprocess.CompletedProcess[str] = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        cwd=root,
    )
    changed = result.stdout.strip().splitlines()
    stacks: set[Path] = set()
    for f in changed:
        parts = Path(f).parts
        if len(parts) >= 2 and parts[0] == STACKS_DIR:
            stacks.add(root / STACKS_DIR / parts[1])
    return sorted(stacks)


def init_stack(stack_dir: Path, backend: bool = True) -> bool:
    stack_name = stack_dir.name
    env = os.environ.copy()

    cmd = ["terraform", "init", "-no-color"]

    if not backend:
        cmd.append("-backend=false")
    else:
        cmd += [
            f"-backend-config=bucket={STATE_BUCKET}",
            f"-backend-config=key={stack_name}/terraform.tfstate",
            f"-backend-config=region={os.environ['AWS_REGION']}",
            "-backend-config=encrypt=true",
            "-backend-config=use_lockfile=true",
        ]

    try:
        result: subprocess.CompletedProcess[str] = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            cwd=stack_dir,
            env=env,
            timeout=120,
        )
        if result.returncode == 0:
            log.info("terraform init ok: %s", stack_name)
            return True
        log.error("terraform init failed: %s\n%s", stack_name, result.stderr.strip())
        return False
    except subprocess.TimeoutExpired:
        log.error("terraform init timed out: %s", stack_name)
        return False
    except Exception:
        log.error("terraform init failed: unexpected error")
        return False


def validate_stack(stack_dir: Path) -> bool:
    stack_name = stack_dir.name
    env = os.environ.copy()

    # terraform fmt check
    fmt: subprocess.CompletedProcess[str] = subprocess.run(
        ["terraform", "fmt", "-check", "-recursive"],
        capture_output=True,
        text=True,
        cwd=stack_dir,
        env=env,
    )
    if fmt.returncode != 0:
        log.error("terraform fmt failed in %s - unformatted files detected", stack_name)
        return False
    log.info("terraform fmt ok: %s", stack_name)

    # terraform validate
    validate: subprocess.CompletedProcess[str] = subprocess.run(
        ["terraform", "validate", "-no-color"],
        capture_output=True,
        text=True,
        cwd=stack_dir,
        env=env,
    )
    if validate.returncode != 0:
        log.error(
            "terraform validate failed in %s: %s", stack_name, validate.stderr.strip()
        )
        return False
    log.info("terraform validate ok: %s", stack_name)

    # tflint init
    tflint_init: subprocess.CompletedProcess[str] = subprocess.run(
        ["tflint", "--init"],
        capture_output=True,
        text=True,
        cwd=stack_dir,
    )
    if tflint_init.returncode != 0:
        log.error(
            "tflint init failed in %s: %s", stack_name, tflint_init.stderr.strip()
        )
        return False
    log.info("tflint init ok: %s", stack_name)

    # tflint
    lint: subprocess.CompletedProcess[str] = subprocess.run(
        ["tflint"],
        capture_output=True,
        text=True,
        cwd=stack_dir,
    )
    if lint.returncode != 0:
        log.error("tflint failed in %s:\n%s", stack_name, lint.stdout.strip())
        return False
    log.info("tflint ok: %s", stack_name)

    return True


def plan_stack(stack_dir: Path, destroy: bool = False) -> bool:
    stack_name = stack_dir.name
    plan_binary = stack_dir / "tfplan.binary"
    plan_json = stack_dir / "tfplan.json"
    env = os.environ.copy()

    plan_args = [
        "terraform",
        "plan",
        "-out",
        str(plan_binary),
        "-no-color",
    ]
    if destroy:
        plan_args.append("-destroy")

    try:
        result: subprocess.CompletedProcess[str] = subprocess.run(
            plan_args,
            capture_output=True,
            text=True,
            cwd=stack_dir,
            env=env,
            timeout=300,
        )
        if result.returncode != 0:
            log.error(
                "terraform plan failed in %s: %s", stack_name, result.stderr.strip()
            )
            return False
        log.info("terraform plan ok: %s", stack_name)
        for line in result.stdout.splitlines():
            log.info(line)
    except subprocess.TimeoutExpired:
        log.error("terraform plan timed out: %s", stack_name)
        return False
    except Exception:
        log.error("terraform plan failed: unexpected error")
        return False

    # convert plan to json
    try:
        show: subprocess.CompletedProcess[str] = subprocess.run(
            ["terraform", "show", "-json", str(plan_binary)],
            capture_output=True,
            text=True,
            cwd=stack_dir,
            env=env,
            timeout=60,
        )
        if show.returncode != 0:
            log.error(
                "terraform show failed in %s: %s", stack_name, show.stderr.strip()
            )
            return False
        plan_json.write_text(show.stdout)
        log.info("tfplan.json generated: %s", stack_name)
    except subprocess.TimeoutExpired:
        log.error("terraform show timed out: %s", stack_name)
        return False
    except Exception:
        log.error("terraform show failed: unexpected error")
        return False

    return True


def execute_stack(stack_dir: Path, destroy: bool = False) -> bool:
    stack_name = stack_dir.name
    plan_binary = stack_dir / "tfplan.binary"
    env = os.environ.copy()

    if not plan_binary.exists():
        log.error("tfplan.binary not found for %s", stack_name)
        return False

    try:
        result: subprocess.CompletedProcess[bytes] = subprocess.run(
            ["terraform", "apply", "-auto-approve", "-no-color", str(plan_binary)],
            cwd=stack_dir,
            env=env,
        )
        if result.returncode != 0:
            log.error("terraform apply failed: %s", stack_name)
            return False
        log.info("terraform apply ok: %s", stack_name)
    except Exception:
        log.error("terraform apply failed: unexpected error")
        return False

    if destroy:
        return True

    # fetch and log outputs
    try:
        out: subprocess.CompletedProcess[str] = subprocess.run(
            ["terraform", "output", "-json"],
            capture_output=True,
            text=True,
            cwd=stack_dir,
            env=env,
        )
        if out.returncode != 0:
            log.warning("terraform output failed: %s", out.stderr.strip())
            return True

        if not out.stdout.strip():
            log.info("no outputs defined for %s", stack_name)
            return True

        outputs = json.loads(out.stdout)
        if not outputs:
            log.info("no outputs defined for %s", stack_name)
            return True

        log.info("outputs for stack: %s", stack_name)
        for key, meta in outputs.items():
            sensitive = meta.get("sensitive", False)
            if sensitive:
                log.info("  %s = [sensitive]", key)
            else:
                value = meta.get("value")
                value_str = (
                    json.dumps(value, indent=2)
                    if isinstance(value, (dict, list))
                    else str(value)
                )
                log.info("  %s = %s", key, value_str)

    except json.JSONDecodeError:
        log.warning("terraform output could not be parsed for %s", stack_name)
    except Exception:
        log.warning("terraform output failed: unexpected error")

    return True


def upload_plan(stack_dir: Path) -> bool:
    stack_name = stack_dir.name
    commit_sha = os.environ["BITBUCKET_COMMIT"]
    sha_prefix = f"s3://{PLAN_BUCKET}/{stack_name}/{commit_sha}"
    latest_prefix = f"s3://{PLAN_BUCKET}/{stack_name}/latest"

    for artifact in ["tfplan.binary", "tfplan.json"]:
        local_path = stack_dir / artifact
        if not local_path.exists():
            log.error("plan artifact not found: %s", local_path)
            return False

        for s3_prefix in [sha_prefix, latest_prefix]:
            destination = f"{s3_prefix}/{artifact}"
            try:
                result: subprocess.CompletedProcess[str] = subprocess.run(
                    ["aws", "s3", "cp", str(local_path), destination],
                    capture_output=True,
                    text=True,
                    timeout=60,
                )
                if result.returncode != 0:
                    log.error(
                        "s3 plan upload failed: %s - %s",
                        destination,
                        result.stderr.strip(),
                    )
                    return False
                log.info("s3 plan upload completed: %s", destination)
            except subprocess.TimeoutExpired:
                log.error("s3 plan upload timed out: %s", destination)
                return False
            except Exception:
                log.error("s3 plan upload failed: unexpected error: %s", destination)
                return False

    return True


def upload_log(stack_dir: Path) -> bool:
    stack_name = stack_dir.name
    commit_sha = os.environ["BITBUCKET_COMMIT"]
    sha_prefix = f"s3://{LOG_BUCKET}/{stack_name}/{commit_sha}"
    latest_prefix = f"s3://{LOG_BUCKET}/{stack_name}/latest"
    local_path = stack_dir / "terraform.log"

    if not local_path.exists():
        log.warning("terraform.log not found for %s - skipping log upload", stack_name)
        return True

    # assume log archive role once, reuse for all uploads
    log_creds = assume_log_archive_role(stack_name)
    if log_creds is None:
        log.error("could not assume log archive role - skipping log upload")
        return False

    env = os.environ.copy()
    env.update(log_creds)

    for s3_prefix in [sha_prefix, latest_prefix]:
        destination = f"{s3_prefix}/terraform.log"
        try:
            result: subprocess.CompletedProcess[str] = subprocess.run(
                ["aws", "s3", "cp", str(local_path), destination],
                capture_output=True,
                text=True,
                timeout=60,
                env=env,
            )
            if result.returncode != 0:
                log.error(
                    "s3 log upload failed: %s - %s", destination, result.stderr.strip()
                )
                return False
            log.info("s3 log upload completed: %s", destination)
        except subprocess.TimeoutExpired:
            log.error("s3 log upload timed out: %s", destination)
            return False
        except Exception:
            log.error("s3 log upload failed: unexpected error: %s", destination)
            return False

    return True


def download_plan(stack_dir: Path) -> bool:
    stack_name = stack_dir.name
    s3_prefix = f"s3://{PLAN_BUCKET}/{stack_name}/latest"

    for artifact in ["tfplan.binary", "tfplan.json"]:
        source = f"{s3_prefix}/{artifact}"
        local_path = stack_dir / artifact
        try:
            result: subprocess.CompletedProcess[str] = subprocess.run(
                ["aws", "s3", "cp", source, str(local_path)],
                capture_output=True,
                text=True,
                timeout=60,
            )
            if result.returncode != 0:
                log.error(
                    "s3 plan download failed: %s - %s", source, result.stderr.strip()
                )
                return False
            log.info("s3 plan download completed: %s", source)
        except subprocess.TimeoutExpired:
            log.error("s3 plan download timed out: %s", source)
            return False
        except Exception:
            log.error("s3 plan download failed: unexpected error: %s", source)
            return False

    return True


def download_policy_bundle(dest_dir: Path) -> Path | None:
    bundle_path = dest_dir / "policies-bundle-latest.tar.gz"
    try:
        result: subprocess.CompletedProcess[str] = subprocess.run(
            [
                "jf",
                "rt",
                "download",
                "iac-generic-tooling-local/policies/policies-bundle-latest.tar.gz",
                str(bundle_path),
                "--server-id",
                JF_SERVER_ID,
                "--flat",
            ],
            capture_output=True,
            text=True,
            timeout=60,
        )
        if result.returncode != 0:
            log.error("policy bundle download failed: %s", result.stderr.strip())
            return None
        log.info("policy bundle downloaded: %s", bundle_path)
        return bundle_path
    except subprocess.TimeoutExpired:
        log.error("policy bundle download timed out")
        return None
    except Exception:
        log.error("policy bundle download failed: unexpected error")
        return None


def eval_policies(stack_dir: Path, bundle_path: Path) -> bool:
    stack_name = stack_dir.name
    plan_json = stack_dir / "tfplan.json"
    governance = stack_dir / "governance.json"

    if not plan_json.exists():
        log.error("tfplan.json not found for %s - cannot evaluate policies", stack_name)
        return False

    base_cmd = [
        "opa",
        "eval",
        "--input",
        str(plan_json),
        "--bundle",
        str(bundle_path),
        "--format",
        "raw",
    ]

    if governance.exists():
        log.info("governance.json found for %s - including in eval", stack_name)
        base_cmd += ["--data", str(governance)]
    else:
        log.info("no governance.json found for %s - skipping exemptions", stack_name)

    # warn evaluation
    log.info("evaluating warn policy rules for %s", stack_name)
    try:
        warn: subprocess.CompletedProcess[str] = subprocess.run(
            base_cmd + ["data.evaluate.warn[_].summary"],
            capture_output=True,
            text=True,
            timeout=60,
        )
        warnings = [line for line in warn.stdout.strip().splitlines() if line.strip()]
        if warnings:
            log.warning("policy evaluation warnings: %s warning(s)", len(warnings))
            for w in warnings:
                log.warning("  %s", w)
        else:
            log.info("policy evaluation passed: %s", stack_name)
    except subprocess.TimeoutExpired:
        log.warning("policy evaluation timed out: %s", stack_name)
    except Exception:
        log.warning("policy evaluation failed: unexpected error")

    # deny evaluation
    mode = "preventive" if ENFORCE_POLICIES else "detective"
    log.info("evaluating deny policy rules for %s (%s mode)", stack_name, mode)
    deny_passed = True
    try:
        deny: subprocess.CompletedProcess[str] = subprocess.run(
            base_cmd + ["data.evaluate.deny[_].summary"],
            capture_output=True,
            text=True,
            timeout=60,
        )
        violations = [line for line in deny.stdout.strip().splitlines() if line.strip()]
        if violations:
            log.error("policy evaluation failed: %s violation(s)", len(violations))
            for v in violations:
                log.error("  %s", v)
            if ENFORCE_POLICIES:
                deny_passed = False
            else:
                log.warning("skipping policy enforcement (%s mode)", mode)
        else:
            log.info("policy evaluation passed: %s", stack_name)
    except subprocess.TimeoutExpired:
        log.error("policy evaluation timed out: %s", stack_name)
        deny_passed = False
    except Exception:
        log.error("policy evaluation failed: unexpected error")
        deny_passed = False

    return deny_passed


def parse_tfplan(stack_dir: Path) -> str:
    # TODO: parse tfplan.json and extract resource change counts into markdown
    return f"Plan generated for stack `{stack_dir.name}`.\n\nReview pipeline logs for full plan details."


def parse_findings(findings: dict[str, object]) -> str:
    # TODO: parse findings and build markdown summary
    secrets: list[dict[str, object]] = findings.get("secrets") or []  # type: ignore
    iac: list[dict[str, object]] = findings.get("iac") or []  # type: ignore
    sast: list[dict[str, object]] = findings.get("sast") or []  # type: ignore

    total = len(secrets) + len(iac) + len(sast)

    if total == 0:
        return "No security findings detected."

    lines = [f"**{total} finding(s) detected.**\n"]
    if secrets:
        lines.append(f"- Secrets: {len(secrets)}")
    if iac:
        lines.append(f"- IaC vulnerabilities: {len(iac)}")
    if sast:
        lines.append(f"- SAST: {len(sast)}")

    lines.append("\nReview pipeline logs for full scan details.")
    return "\n".join(lines)


def post_pr_comment(title: str, body: str) -> bool:
    workspace = os.environ.get("BITBUCKET_WORKSPACE")
    repo = os.environ.get("BITBUCKET_REPO_SLUG")
    pr_id = os.environ.get("BITBUCKET_PR_ID")
    token = os.environ.get("BITBUCKET_BOT_TOKEN")

    if not all([workspace, repo, pr_id, token]):
        log.warning("pr comment skipped - missing bitbucket context")
        log.warning("workspace : %s", workspace or "n/a")
        log.warning("repo      : %s", repo or "n/a")
        log.warning("pr_id     : %s", pr_id or "n/a")
        log.warning("token     : %s", "(set, hidden)" if token else "n/a")
        return False

    payload = json.dumps({"content": {"raw": f"### {title}\n\n{body}"}}).encode()

    conn: http.client.HTTPSConnection | None = None
    try:
        conn = http.client.HTTPSConnection(
            "api.bitbucket.org",
            timeout=15,
            context=ssl.create_default_context(),
        )
        conn.request(
            "POST",
            f"/2.0/repositories/{workspace}/{repo}/pullrequests/{pr_id}/comments",
            body=payload,
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            },
        )
        response = conn.getresponse()
        if response.status == 201:
            log.info("pr comment posted: %s", title)
            return True
        log.error("pr comment failed: http %s", response.status)
        return False
    except Exception:
        log.error("pr comment failed: unexpected error")
        return False
    finally:
        if conn:
            conn.close()


# Command handlers -------------------------------------------------------------


def cmd_run_diagnostics() -> None:
    log.info("running diagnostics")

    # bitbucket context
    log.info("build number  : %s", os.environ.get("BITBUCKET_BUILD_NUMBER", "n/a"))
    log.info("branch        : %s", os.environ.get("BITBUCKET_BRANCH", "n/a"))
    log.info("commit        : %s", os.environ.get("BITBUCKET_COMMIT", "n/a"))
    log.info("runner uuid   : %s", os.environ.get("BITBUCKET_RUNNER_UUID", "n/a"))
    log.info("repo          : %s", os.environ.get("BITBUCKET_REPO_SLUG", "n/a"))

    # runner environment
    log.info("python        : %s", sys.version.splitlines()[0])
    log.info("platform      : %s", sys.platform)
    log.info("hostname      : %s", socket.gethostname())
    log.info("fqdn          : %s", socket.getfqdn())
    log.info("ip address    : %s", socket.gethostbyname(socket.gethostname()))
    log.info("username      : %s", os.environ.get("USER") or os.environ.get("USERNAME"))
    log.info("cwd           : %s", os.getcwd())

    # tool versions
    for t, v in REQUIRED_TOOLS:
        if not shutil.which(t):
            log.error("tool missing: %s", t)
            continue
        result: subprocess.CompletedProcess[str] = subprocess.run(
            [t, v],
            capture_output=True,
            text=True,
            timeout=30,
        )
        lines = (result.stdout or result.stderr).strip().splitlines()
        log.info("tool found: %s", t)
        for line in lines:
            if line:
                log.info("  %s", line)

    log.info("diagnostics complete")


def cmd_run_scan() -> None:
    log.info("running security scan")

    root = get_repo_root()

    env = os.environ.copy()
    env["JFROG_CLI_LOG_LEVEL"] = "ERROR"
    env["JFROG_CLI_RELEASES_REPO"] = f"{JF_SERVER_ID}/{JF_PUB_RELEASES_REPO}"

    result: subprocess.CompletedProcess[str] = subprocess.run(
        [
            "jf",
            "audit",
            "--sca",
            "--sast",
            "--iac",
            "--secrets",
            "--validate-secrets",
            "--format",
            "simple-json",
            "--server-id",
            JF_SERVER_ID,
            "--exclusions",
            "*.git*",
        ],
        capture_output=True,
        text=True,
        cwd=root,
        env=env,
    )

    if not result.stdout.strip():
        log.warning("jf audit returned no output")
        post_pr_comment(
            "Security Scan Warning",
            "The security scan returned no output. Check the pipeline logs for details.",
        )
        return

    try:
        findings: dict[str, object] = json.loads(result.stdout)
    except json.JSONDecodeError:
        log.error("jf audit output could not be parsed")
        post_pr_comment(
            "Security Scan Warning",
            "The security scan output could not be parsed. Check the pipeline logs for details.",
        )
        return

    log.info("security scan completed")
    post_pr_comment("Security Scan Completed", parse_findings(findings))


def cmd_validate_stack(stack_dir: Path, intent: str) -> None:
    log.info("validating stack: %s", stack_dir.name)

    if intent == "destroy":
        log.info(
            "stack '%s' is marked for destroy - skipping validation", stack_dir.name
        )
        sys.exit(0)

    rc_file = write_terraformrc()
    os.environ["TF_CLI_CONFIG_FILE"] = str(rc_file)
    os.environ["TF_TOKEN_hcassc_jfrog_io"] = os.environ["JFRW_TOKEN"]

    passed = False
    try:
        if not init_stack(stack_dir, backend=False):
            post_pr_comment(
                "Stack Validation Failed",
                f"Stack `{stack_dir.name}` failed to initialise.\n\n"
                "Check the pipeline logs for details.",
            )
            sys.exit(1)

        if not validate_stack(stack_dir):
            post_pr_comment(
                "Stack Validation Failed",
                f"Stack `{stack_dir.name}` failed to validate.\n\n"
                "Check the pipeline logs for details.",
            )
            sys.exit(1)

        passed = True
    finally:
        if passed:
            post_pr_comment(
                "Stack Validation Passed",
                f"Stack `{stack_dir.name}` passed all validation checks.\n\n"
                "- **OK**: terraform fmt\n"
                "- **OK**: terraform validate\n"
                "- **OK**: tflint",
            )
        try:
            rc_file.unlink(missing_ok=True)
        except OSError:
            log.warning("could not remove temp terraformrc: %s", rc_file)

    log.info("stack validation passed: %s", stack_dir.name)


def cmd_plan_stack(stack_dir: Path, intent: str) -> None:
    log.info("planning stack: %s intent: %s", stack_dir.name, intent)

    rc_file = write_terraformrc()
    os.environ["TF_CLI_CONFIG_FILE"] = str(rc_file)
    os.environ["TF_TOKEN_hcassc_jfrog_io"] = os.environ["JFRW_TOKEN"]
    os.environ["TF_LOG"] = "INFO"
    os.environ["TF_LOG_PATH"] = str(stack_dir / "terraform.log")

    bundle_path: Path | None = None
    passed = False
    try:
        if not init_stack(stack_dir, backend=True):
            post_pr_comment(
                "Stack Plan Failed",
                f"Stack `{stack_dir.name}` failed to initialise.\n\n"
                "Check the pipeline logs for details.",
            )
            sys.exit(1)

        if not check_tfbackend(stack_dir):
            post_pr_comment(
                "Stack Plan Failed",
                f"Stack `{stack_dir.name}` does not have a valid S3 backend configured.\n\n"
                'Ensure the stack declares an empty `backend "s3" {}` in terraform block.',
            )
            sys.exit(1)

        if not plan_stack(stack_dir, destroy=intent == "destroy"):
            post_pr_comment(
                "Stack Plan Failed",
                f"Stack `{stack_dir.name}` failed to plan.\n\n"
                "Check the pipeline logs for details.",
            )
            sys.exit(1)

        # download policy bundle and evaluate
        bundle_path = download_policy_bundle(stack_dir)
        if bundle_path is None:
            post_pr_comment(
                "Stack Plan Failed",
                f"Stack `{stack_dir.name}` plan succeeded but policy bundle could not be downloaded.\n\n"
                "Check the pipeline logs for details.",
            )
            sys.exit(1)

        if not eval_policies(stack_dir, bundle_path):
            post_pr_comment(
                "Stack Plan Failed — Policy Violations Detected",
                f"Stack `{stack_dir.name}` plan was blocked by policy violations.\n\n"
                "Check the pipeline logs for full violation details.",
            )
            sys.exit(1)

        if not upload_plan(stack_dir):
            post_pr_comment(
                "Stack Plan Failed",
                f"Stack `{stack_dir.name}` plan succeeded but failed to upload artifacts to S3.\n\n"
                "Check the pipeline logs for details.",
            )
            sys.exit(1)

        passed = True

    finally:
        if passed:
            post_pr_comment("Stack Plan Completed", parse_tfplan(stack_dir))
        if bundle_path and bundle_path.exists():
            try:
                bundle_path.unlink()
                log.info("policy bundle cleaned up")
            except OSError:
                log.warning("could not remove policy bundle: %s", bundle_path)
        upload_log(stack_dir)
        try:
            rc_file.unlink(missing_ok=True)
        except OSError:
            log.warning("could not remove temp terraformrc: %s", rc_file)

    log.info("stack plan completed: %s", stack_dir.name)


def cmd_execute_stack(stack_dir: Path, intent: str) -> None:
    log.info("executing stack: %s intent: %s", stack_dir.name, intent)

    rc_file = write_terraformrc()
    os.environ["TF_CLI_CONFIG_FILE"] = str(rc_file)
    os.environ["TF_TOKEN_hcassc_jfrog_io"] = os.environ["JFRW_TOKEN"]
    os.environ["TF_LOG"] = "INFO"
    os.environ["TF_LOG_PATH"] = str(stack_dir / "terraform.log")

    try:
        if not init_stack(stack_dir, backend=True):
            sys.exit(1)

        if not download_plan(stack_dir):
            sys.exit(1)

        if not execute_stack(stack_dir, destroy=intent == "destroy"):
            sys.exit(1)

    finally:
        upload_log(stack_dir)
        try:
            rc_file.unlink(missing_ok=True)
        except OSError:
            log.warning("could not remove temp terraformrc: %s", rc_file)

    log.info("stack execution completed: %s", stack_dir.name)


# Main entry point -------------------------------------------------------------


def main():
    parser = argparse.ArgumentParser(
        prog="core-pipeline",
        description="iac-core pipeline processor",
    )

    sub = parser.add_subparsers(dest="command", metavar="<command>")
    sub.required = True

    sub.add_parser("run-diagnostics", help="run diagnostics on the runner environment")
    sub.add_parser("run-scan", help="run security scan on stacks")
    sub.add_parser("validate-stack", help="fmt, validate and lint changed stack")
    sub.add_parser("plan-stack", help="plan changed stack and upload to S3")
    sub.add_parser("execute-stack", help="apply or destroy changed stack")

    args = parser.parse_args()

    # tools check
    missing_tools = [t for t, _ in REQUIRED_TOOLS if not shutil.which(t)]
    if missing_tools:
        for t in missing_tools:
            log.error("tool missing: %s", t)
        sys.exit(1)
    log.info("tools found: %s", ", ".join(t for t, _ in REQUIRED_TOOLS))

    # env check
    missing_vars = [v for v in REQUIRED_ENV_VARS if not os.environ.get(v)]
    if missing_vars:
        for v in missing_vars:
            log.error("missing env var: %s", v)
        sys.exit(1)
    log.info("env vars found: %s", ", ".join(REQUIRED_ENV_VARS))

    # setup jfrog
    if not setup_jfrog():
        sys.exit(1)

    # handle non-stack commands
    if args.command == "run-scan":
        cmd_run_scan()
        sys.exit(0)
    elif args.command == "run-diagnostics":
        cmd_run_diagnostics()
        sys.exit(0)

    # aws auth check
    if not check_aws():
        sys.exit(1)

    # stack discovery
    root = get_repo_root()
    changed = get_changed_stacks(root)

    if not changed:
        log.info("no stack changes detected - nothing to do")
        sys.exit(0)

    # filter to only stacks with a sentinel
    actionable = [
        s
        for s in changed
        if (s / ".do-deploy").exists() or (s / ".do-destroy").exists()
    ]

    if not actionable:
        log.info("no actionable stacks found - nothing to do")
        sys.exit(0)

    if len(actionable) > 1:
        log.error("only one stack can be actioned at a time, found %s", len(actionable))
        log.error("marked stacks: %s", ", ".join(s.name for s in actionable))
        sys.exit(1)

    stack_dir = actionable[0]
    stack_name = stack_dir.name

    # intent resolution
    has_deploy = (stack_dir / ".do-deploy").exists()
    has_destroy = (stack_dir / ".do-destroy").exists()

    if has_deploy and has_destroy:
        log.error("stack %s has both .do-deploy and .do-destroy sentinels", stack_name)
        sys.exit(1)

    intent = "deploy" if has_deploy else "destroy"
    log.info("actioning stack: %s intent: %s", stack_name, intent)

    if args.command == "validate-stack":
        cmd_validate_stack(stack_dir, intent)
    elif args.command == "plan-stack":
        cmd_plan_stack(stack_dir, intent)
    elif args.command == "execute-stack":
        cmd_execute_stack(stack_dir, intent)
    else:
        parser.print_help()
        sys.exit(1)


# Execution entry point --------------------------------------------------------
if __name__ == "__main__":
    main()
