"""Publish deploy outputs to SSM + format GitHub PR comments (D-050).

Two canonical mechanisms:
  1. SSM Parameter Store (SecureString, KMS-encrypted) for runtime-injectable
     values — resources that need to read outputs at runtime (e.g. an ECS
     task reading its S3 bucket name).
  2. GitHub PR comment / job summary for human-readable outputs (connection
     strings, ALB DNS, S3 bucket URL, CloudFront domain). No raw secrets in
     the comment — only non-sensitive outputs (DNS names, ARNs, bucket names).

The namespace is /nova/{environment}/{contractId}/{output_name} so consumers
can query their own outputs via aws ssm get-parameter --name /nova/dev/<id>/...
(REQ-161, P3: migrated from /acdl/... ; scripts/migrate_ssm_paths.py copies
existing /acdl/... parameters to /nova/... and deletes the old ones.)
"""

import json
import os
import sys
import urllib.error
import urllib.request

try:
    import boto3
    from botocore.exceptions import ClientError
except ImportError:
    boto3 = None
    ClientError = Exception  # type: ignore[assignment,misc]

# Repo root on sys.path so `from core import env` resolves to THIS package
# when run as a script (avoids editable-installed third-party `core` shadow).
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _REPO_ROOT not in sys.path:
    sys.path.insert(0, _REPO_ROOT)

from core import env as _envhelper

SSM_PREFIX = "/nova"
KMS_KEY_ID_ENV = "NOVA_KMS_KEY_ID"

# P14 (REQ-178): SAFE_OUTPUT_NAMES is schema-driven (derived from
# modules/l1/*/interface.json outputs that don't have sensitive:true).
# Falls back to the hardcoded set if the interfaces can't be read.
_HARDCODED_SAFE_OUTPUTS = {
    "distribution_domain_name",
    "bucket_arn",
    "bucket_name",
    "bucket_regional_domain_name",
    "web_acl_arn",
    "lb_arn",
    "listener_arn",
    "target_group_arn",
    "service_arn",
    "cluster_arn",
    "repository_url",
    "db_endpoint",
    "db_arn",
    "distribution_arn",
    "vpc_id",
    "subnet_ids",
}


def _load_safe_output_names():
    """Derive the safe-output allowlist from interface.json outputs.

    P14 (REQ-178): scan modules/l1/*/interface.json; an output is safe if
    its spec does not set sensitive:true. Falls back to the hardcoded set
    if no interfaces are readable.
    """
    import json
    from pathlib import Path
    root = Path(__file__).resolve().parent.parent
    safe = set()
    try:
        for iface in (root / "modules" / "l1").glob("*/interface.json"):
            d = json.loads(iface.read_text())
            outs = d.get("outputs", {})
            if isinstance(outs, dict):
                for name, spec in outs.items():
                    if not (isinstance(spec, dict) and spec.get("sensitive")):
                        safe.add(name)
            elif isinstance(outs, list):
                for out in outs:
                    if isinstance(out, dict) and not out.get("sensitive"):
                        safe.add(out.get("name", ""))
    except (OSError, ValueError):
        pass
    return safe or _HARDCODED_SAFE_OUTPUTS


SAFE_OUTPUT_NAMES = _load_safe_output_names()


def _ssm_client():
    if boto3 is None:
        raise RuntimeError("boto3 is required for SSM publishing")
    return boto3.client("ssm")


def _kms_key_id():
    """Return the KMS key ID for SSM SecureString encryption.

    P1-3: Fail loud when NOVA_KMS_KEY_ID is not set — silently falling back
    to the AWS-managed key (`alias/aws/ssm`) was a security gap. The platform
    CMK must be explicitly configured. Set NOVA_ALLOW_DEFAULT_KMS=1 to use
    the AWS-managed key as an escape hatch for local testing. (Dual-read
    via core/env.py: NOVA_* preferred, ACDL_* fallback until P5.)
    """
    key_id = _envhelper.get_env("KMS_KEY_ID")
    if key_id:
        return key_id
    if _envhelper.get_env("ALLOW_DEFAULT_KMS") == "1":
        return "alias/aws/ssm"
    raise RuntimeError(
        f"{KMS_KEY_ID_ENV} is not set — refusing to use the AWS-managed SSM key "
        f"silently. Set {KMS_KEY_ID_ENV} to your platform CMK ARN, or set "
        f"NOVA_ALLOW_DEFAULT_KMS=1 (ACDL_ALLOW_DEFAULT_KMS=1 fallback) to use "
        f"alias/aws/ssm (escape hatch for local testing)."
    )


def publish_to_ssm(outputs, environment, contract_id):
    """Write each output to SSM Parameter Store as a SecureString.

    Returns a dict of {output_name: parameter_arn} for successful writes.
    Skips None values and empty strings.
    """
    if boto3 is None:
        return {}
    client = _ssm_client()
    kms_key = _kms_key_id()
    results = {}
    for name, value in outputs.items():
        if value is None:
            continue
        if isinstance(value, str) and not value.strip():
            continue
        param_name = f"{SSM_PREFIX}/{environment}/{contract_id}/{name}"
        try:
            client.put_parameter(
                Name=param_name,
                Value=str(value),
                Type="SecureString",
                KeyId=kms_key,
                Overwrite=True,
            )
            results[name] = param_name
        except (ClientError, OSError) as e:
            # P4 (REQ-168): narrow from bare `except Exception` to AWS +
            # OS errors. Don't fail the pipeline if one output fails to
            # publish, but log it with context.
            import sys
            print(f"WARNING: SSM put_parameter failed for {name}: {type(e).__name__}: {e}", file=sys.stderr)
            results[name] = None
    return results


def format_comment(outputs, environment, contract_id, ssm_results=None):
    """Format a GitHub PR comment / job summary with human-readable outputs.

    Only non-sensitive outputs (SAFE_OUTPUT_NAMES) are included. Sensitive
    outputs are noted as 'published to SSM' without their values.
    """
    lines = [
        f"### Nova Deploy Outputs ({environment})",
        "",
        f"**Contract:** `{contract_id}`",
        f"**Environment:** `{environment}`",
        "",
        "| Output | Value | SSM |",
        "|--------|-------|-----|",
    ]
    for name, value in sorted(outputs.items()):
        if value is None:
            continue
        if isinstance(value, str) and not value.strip():
            continue
        safe = name in SAFE_OUTPUT_NAMES
        display = str(value) if safe else "`(published to SSM)`"
        ssm_path = ""
        if ssm_results and ssm_results.get(name):
            ssm_path = f"`{ssm_results[name]}`"
        elif ssm_results is not None:
            ssm_path = "—"
        lines.append(f"| `{name}` | {display} | {ssm_path} |")
    lines.append("")
    lines.append("> Sensitive outputs are available via `aws ssm get-parameter --name /nova/" + environment + "/" + contract_id + "/<output_name>` (KMS-encrypted SecureString).")
    return "\n".join(lines)


def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
    """Post a comment to a GitHub PR via the GitHub API.

    Uses GITHUB_TOKEN from env if token is None. Uses GITHUB_REPOSITORY if
    repo is None. Uses the PR number from the GITHUB_REF env if pr_number is
    None (extracts from refs/pull/<N>/merge). No-op if not in a PR context.
    """
    if token is None:
        token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
    if repo is None:
        repo = os.environ.get("GITHUB_REPOSITORY", "")
    if pr_number is None:
        ref = os.environ.get("GITHUB_REF", "")
        if "refs/pull/" in ref:
            try:
                pr_number = int(ref.split("/")[2])
            except (IndexError, ValueError):
                pass
    if not token or not repo or not pr_number:
        return False  # not in a PR context or no token
    try:
        url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
        data = json.dumps({"body": comment_text}).encode()
        req = urllib.request.Request(url, data=data, method="POST")
        req.add_header("Authorization", f"token {token}")
        req.add_header("Accept", "application/vnd.github+json")
        urllib.request.urlopen(req, timeout=10)
        return True
    except (OSError, urllib.error.URLError, urllib.error.HTTPError) as e:
        # P4 (REQ-168): narrow from bare `except Exception` to network +
        # HTTP errors. Don't fail the pipeline if the PR comment can't be
        # posted, but log it with context.
        import sys
        print(f"WARNING: GitHub PR comment failed: {type(e).__name__}: {e}", file=sys.stderr)
        return False


if __name__ == "__main__":
    # CLI: output_publisher.py <outputs.json> <environment> <contract_id>
    if len(sys.argv) != 4:
        print("usage: output_publisher.py <outputs.json> <environment> <contract-id>", file=sys.stderr)
        sys.exit(2)
    with open(sys.argv[1]) as f:
        outputs = json.load(f)
    env = sys.argv[2]
    cid = sys.argv[3]
    ssm_results = publish_to_ssm(outputs, env, cid)
    comment = format_comment(outputs, env, cid, ssm_results)
    print(comment)