#!/usr/bin/env python3
"""One-owner Deployment rollout, business verification, guarded image rollback."""
import argparse
import json
import os
from pathlib import Path
import re
import subprocess


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--context', required=True)
    parser.add_argument('--environment', choices=['staging', 'production'], required=True)
    parser.add_argument('--image-file', default='audit/image.txt')
    args = parser.parse_args()
    image = Path(args.image_file).read_text().strip()
    if not re.fullmatch(r'[a-zA-Z0-9./:_-]+@sha256:[0-9a-f]{64}', image):
        raise ValueError('An immutable image digest is required')
    base = ['kubectl', '--context', args.context, '-n', 'orders']
    audit = Path('audit')
    audit.mkdir(exist_ok=True)
    receipt = {'environment': args.environment, 'context': args.context,
               'deployment': 'orders-api', 'newImage': image, 'result': 'preflight'}
    receipt_path = audit / (args.environment + '.json')

    def record():
        receipt_path.write_text(json.dumps(receipt, indent=2) + '\n')

    def get():
        return json.loads(subprocess.check_output(base + ['get', 'deployment', 'orders-api', '-o', 'json']))

    def patch(current, expected, replacement):
        containers = current['spec']['template']['spec']['containers']
        indexes = [i for i, c in enumerate(containers) if c['name'] == 'api']
        if len(indexes) != 1:
            raise ValueError('Expected exactly one api container')
        path = f'/spec/template/spec/containers/{indexes[0]}/image'
        operations = [
            {'op': 'test', 'path': '/metadata/resourceVersion', 'value': current['metadata']['resourceVersion']},
            {'op': 'test', 'path': path, 'value': expected},
            {'op': 'replace', 'path': path, 'value': replacement},
        ]
        subprocess.run(base + ['patch', 'deployment', 'orders-api', '--type=json', '-p', json.dumps(operations)], check=True)

    def ready():
        subprocess.run(base + ['rollout', 'status', 'deployment/orders-api', '--timeout=10m'], check=True)

    before = get()
    old = next(c['image'] for c in before['spec']['template']['spec']['containers'] if c['name'] == 'api')
    if not re.fullmatch(r'[a-zA-Z0-9./:_-]+@sha256:[0-9a-f]{64}', old):
        raise ValueError('Current image must also use a digest for reliable rollback')
    if image == old:
        raise ValueError('Already deployed; verify existing release instead of creating a false new release')
    receipt['oldImage'] = old
    receipt['beforeResourceVersion'] = before['metadata']['resourceVersion']
    record()
    # Never archive the full Deployment: it might contain inline application secrets.
    patch(before, old, image)
    receipt['result'] = 'deployed-unverified'
    record()
    try:
        ready()
        # Fixed, reviewed business test; never take a user-supplied command parameter.
        subprocess.run(['bash', 'ci/verify-business.sh', args.environment], check=True, timeout=300)
        receipt['result'] = 'passed'
    except (subprocess.SubprocessError, OSError):
        receipt['result'] = 'verification-failed'
        record()
        # Concurrent changes cause the test/patch to fail instead of overwriting a new release.
        try:
            patch(get(), image, old)
            ready()
            subprocess.run(['bash', 'ci/verify-business.sh', args.environment], check=True, timeout=300)
            receipt['result'] = 'rolled-back-and-verified'
        except (subprocess.SubprocessError, OSError, ValueError):
            receipt['result'] = 'rollback-failed-needs-operator'
            record()
            raise
        record()
        raise
    record()


if __name__ == '__main__':
    main()
