#!/usr/bin/env bash
#
# Export Laravel secrets for AWS Secrets Manager from the local .env and credential files.
#
# Usage (run on the production/staging server):
#   ./scripts/generate-secrets-env.sh
#   ./scripts/generate-secrets-env.sh --env /var/www/html/.env
#   ./scripts/generate-secrets-env.sh --env .env --output /tmp/secrets.env
#   ./scripts/generate-secrets-env.sh --project myapp --environment production
#   ./scripts/generate-secrets-env.sh --skip-env-update   # only write secrets output
#
# Output is written as secrets.env (.env format) with mode 600.
# Secret values are never printed to stdout.
#
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"

ENV_FILE="${PROJECT_ROOT}/.env"
OUTPUT_FILE="${PROJECT_ROOT}/secrets.env"
PROJECT_NAME="Shakker"
ENVIRONMENT="production"
SKIP_ENV_UPDATE="false"

usage() {
    sed -n '3,13p' "$0" | sed 's/^# \{0,1\}//'
    exit "${1:-0}"
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        --env | --env-file)
            ENV_FILE="$2"
            shift 2
            ;;
        --output | -o)
            OUTPUT_FILE="$2"
            shift 2
            ;;
        --project | --project-name)
            PROJECT_NAME="$2"
            shift 2
            ;;
        --environment | --env-name)
            ENVIRONMENT="$2"
            shift 2
            ;;
        --skip-env-update)
            SKIP_ENV_UPDATE="true"
            shift
            ;;
        -h | --help)
            usage 0
            ;;
        *)
            echo "Unknown option: $1" >&2
            usage 1
            ;;
    esac
done

if [[ ! -f "$ENV_FILE" ]]; then
    echo "Error: .env file not found: $ENV_FILE" >&2
    exit 1
fi

if ! command -v python3 >/dev/null 2>&1; then
    echo "Error: python3 is required." >&2
    exit 1
fi

python3 - "$ENV_FILE" "$OUTPUT_FILE" "$ENVIRONMENT" "$SKIP_ENV_UPDATE" "$PROJECT_ROOT" "$PROJECT_NAME" <<'PY'
import json
import re
import shutil
import sys
from datetime import datetime
from pathlib import Path

env_file = Path(sys.argv[1])
output_file = Path(sys.argv[2])
environment = sys.argv[3]
skip_env_update = sys.argv[4].lower() == "true"
project_root = Path(sys.argv[5]).resolve()
project_name = sys.argv[6]

# Keep in sync with App\Support\AwsSecretsLoader::{SCALAR_KEYS,PEM_KEYS,GOOGLE_CREDENTIAL_KEYS}
# Do not export AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY — S3 uses the EC2 instance profile.
SCALAR_KEYS = [
    "APP_KEY",
    "DB_PASSWORD",
    "MAIL_PASSWORD",
    "APPSTORE_PASSWORD",
    "APPSTORE_ISSUER_ID",
    "APPSTORE_PRIVATE_KEY_ID",
    "PUSH_NOTIFICATION_SERVER_KEY",
    "STRIPE_SECRET_KEY",
    "STRIPE_PUBLIC_KEY",
    "TWILIO_AUTH_TOKEN",
    "CRIMINAL_CHECK_PASSWORD",
    "RECAPTCHA_SECRET_KEY",
    "DATABASE_URL",
    "MAIL_SMTP_PASS",
    "FIREBASE_PRIVATE_KEY",
    "ADMIN_SESSION_SECRET",
    "AUTH_ACCESS_TOKEN_SECRET",
    "AUTH_REFRESH_TOKEN_SECRET",
    "GOOGLE_MAPS_API_KEY",
    "AUTH_GOOGLE_CLIENT_IDS",
    "GOOGLE_KEY"
]

OPTIONAL_SCALAR_KEYS = [
    "PUSHER_APP_ID",
    "PUSHER_APP_KEY",
    "PUSHER_APP_SECRET",
    "REDIS_PASSWORD",
    "TWILIO_SID",
    "TWILIO_NUMBER",
    "CRIMINAL_CHECK_ACCOUNT_ID",
    "PROJECT_ID",
]

# .env key -> secret key for inline file content
FILE_KEYS = {
    "APPSTORE_PRIVATE_KEY": "APPSTORE_PRIVATE_KEY",
    "GOOGLE_APPLICATION_CREDENTIALS": "GOOGLE_APPLICATION_CREDENTIALS",
    "GOOGLE_FIREBASE_CREDENTIALS": "GOOGLE_FIREBASE_CREDENTIALS",
}

KNOWN_FILENAMES = {
    "APPSTORE_PRIVATE_KEY": ["SubscriptionKey_H9D38A564N.p8"],
    "GOOGLE_APPLICATION_CREDENTIALS": ["google-app-credentials.json"],
    "GOOGLE_FIREBASE_CREDENTIALS": ["service-account-file.json"],
}

NEEDS_QUOTING = re.compile(r'[\s#"$\\]')

# Scalar keys removed from .env after export to secrets.env.
# Credential file paths (FILE_KEYS) stay in .env — only inline content moves to secrets.env.
KEYS_TO_REMOVE_FROM_ENV = set(SCALAR_KEYS + OPTIONAL_SCALAR_KEYS)


def parse_env_from_text(text: str) -> dict[str, str]:
    data: dict[str, str] = {}
    for raw_line in text.splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        if "=" not in line:
            continue
        key, value = line.split("=", 1)
        key = key.strip()
        value = value.strip()
        if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
            value = value[1:-1]
        data[key] = value
    return data


def is_empty(value: str | None) -> bool:
    return value is None or value.strip() == "" or value.strip().lower() == "null"


def format_scalar(key: str, value: str) -> str:
    if "\n" in value or NEEDS_QUOTING.search(value):
        escaped = value.replace("\\", "\\\\").replace('"', '\\"')
        return f'{key}="{escaped}"'
    return f"{key}={value}"


def resolve_credential_path(path_str: str, env_key: str) -> Path:
    raw = Path(path_str)
    candidates: list[Path] = []

    if raw.is_absolute():
        candidates.append(raw)
    else:
        candidates.append((project_root / raw).resolve())

    basename = raw.name
    if basename:
        candidates.append(project_root / basename)

    for name in KNOWN_FILENAMES.get(env_key, []):
        candidates.append(project_root / name)

    seen: set[Path] = set()
    for candidate in candidates:
        if candidate in seen:
            continue
        seen.add(candidate)
        if candidate.is_file():
            return candidate

    searched = ", ".join(str(p) for p in seen)
    raise FileNotFoundError(f"searched: {searched}")


def read_file_content(path_str: str, env_key: str) -> tuple[str, Path]:
    path = resolve_credential_path(path_str, env_key)
    content = path.read_text(encoding="utf-8", errors="replace").strip()
    return content, path


def normalize_json_content(raw: str) -> str:
    try:
        parsed = json.loads(raw)
        return json.dumps(parsed, separators=(",", ":"))
    except json.JSONDecodeError:
        return raw


def format_multiline(key: str, value: str) -> str:
    escaped = value.replace("\\", "\\\\").replace('"', '\\"')
    return f'{key}="{escaped}"'


def extract_env_key(line: str) -> str | None:
    stripped = line.strip()
    if not stripped or stripped.startswith("#") or "=" not in stripped:
        return None
    return stripped.split("=", 1)[0].strip()


def strip_secrets_from_env(original_text: str) -> tuple[str, int]:
    kept_lines: list[str] = []
    removed_count = 0

    for line in original_text.splitlines():
        key = extract_env_key(line)
        if key and key in KEYS_TO_REMOVE_FROM_ENV:
            removed_count += 1
            continue
        kept_lines.append(line)

    while kept_lines and kept_lines[-1].strip() == "":
        kept_lines.pop()

    return "\n".join(kept_lines) + "\n", removed_count


original_env_text = env_file.read_text(encoding="utf-8", errors="replace")
env = parse_env_from_text(original_env_text)
lines: list[str] = []
warnings: list[str] = []
written_keys: list[str] = []

timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
backup_stamp = datetime.now().strftime("%Y%m%d%H%M%S")
lines.extend(
    [
        f"# {project_name} — {environment} secrets",
        f"# Project: {project_name}",
        f"# Environment: {environment}",
        f"# Generated on server at {timestamp}",
        "# DO NOT COMMIT. Deliver via a secure channel only.",
        "",
    ]
)

for key in SCALAR_KEYS:
    value = env.get(key, "")
    if is_empty(value):
        warnings.append(f"Missing or empty scalar: {key}")
        continue
    lines.append(format_scalar(key, value))
    written_keys.append(key)

lines.append("")
lines.append("# -----------------------------------------------------------------------------")
lines.append("# Inline credential file content")
lines.append("# -----------------------------------------------------------------------------")

for env_key, secret_key in FILE_KEYS.items():
    path_value = env.get(env_key, "")
    if is_empty(path_value):
        warnings.append(f"Skipped {secret_key}: {env_key} not set in .env")
        continue

    try:
        content, resolved_path = read_file_content(path_value, env_key)
    except FileNotFoundError as exc:
        warnings.append(f"Skipped {secret_key}: file not found ({exc})")
        continue

    if is_empty(content):
        warnings.append(f"Skipped {secret_key}: file is empty ({resolved_path})")
        continue

    if secret_key.startswith("GOOGLE_"):
        content = normalize_json_content(content)

    lines.append("")
    if str(resolved_path) != path_value:
        lines.append(f"# source: {resolved_path} (resolved from .env path: {path_value})")
    else:
        lines.append(f"# source: {resolved_path}")
    lines.append(format_multiline(secret_key, content))
    written_keys.append(secret_key)

optional_written = False
optional_lines: list[str] = []
for key in OPTIONAL_SCALAR_KEYS:
    value = env.get(key, "")
    if is_empty(value):
        continue
    optional_lines.append(format_scalar(key, value))
    written_keys.append(key)
    optional_written = True

if optional_written:
    lines.append("")
    lines.append("# -----------------------------------------------------------------------------")
    lines.append("# Optional")
    lines.append("# -----------------------------------------------------------------------------")
    lines.extend(optional_lines)

output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
output_file.chmod(0o600)

print(f"Wrote {output_file} ({output_file.stat().st_size} bytes, mode 600)")
print(f"Project: {project_name}")
print(f"Environment: {environment}")
print(f"Source .env: {env_file}")
print(f"Keys written: {len(written_keys)}")

if not skip_env_update:
    backup_path = env_file.with_name(f".env.bak.{backup_stamp}")
    original_mode = env_file.stat().st_mode
    shutil.copy2(env_file, backup_path)
    backup_path.chmod(original_mode)

    stripped_text, removed_count = strip_secrets_from_env(original_env_text)
    header = (
        f"# Non-secret config only. Secrets exported to {output_file.name} on {timestamp}.\n"
        f"# Backup: {backup_path.name}\n"
    )
    if not stripped_text.startswith("# Non-secret config only."):
        stripped_text = header + stripped_text

    env_file.write_text(stripped_text, encoding="utf-8")
    env_file.chmod(original_mode)

    print(f"Backed up .env to {backup_path}")
    print(f"Updated {env_file} (removed {removed_count} secret key lines)")
else:
    print("Skipped .env backup/update (--skip-env-update)")

if warnings:
    print("Warnings:")
    for warning in warnings:
        print(f"  - {warning}")
PY
