#!/usr/bin/python3

'''
    Convenient wrapper around `oc process/create/replace`. Can be run multiple
    times; subsequent runs will replace existing resources.

    Example usage:
        ./deploy --update \
            --pipeline https://github.com/jlebon/fedora-coreos-pipeline \
            --config https://github.com/jlebon/fedora-coreos-config@wip \
            --cosa-img quay.io/jlebon/coreos-assembler:master
            # OR
            --cosa https://github.com/jlebon/coreos-assembler

    Deleting developer pipeline:
        ./deploy --delete-devel
'''

import os
import sys
import json
import yaml
import argparse
import subprocess


def main():
    args = parse_args()

    if targeting_official_namespace() and not args.official:
        eprint("Refusing to create developer resource in official namespace.")
        eprint("Use --official to create official resources.")
        return 1

    resources = process_template(args)

    if args.update:
        update_resources(args, resources)

        # hack the name; should extract from manifest
        pipeline_name = "fedora-coreos-pipeline"
        if not args.official:
            pipeline_name = f"{args.prefix}{pipeline_name}"

        if args.start:
            print("Starting:")
            out = subprocess.run(['oc', 'start-build', pipeline_name],
                                 check=True, stdout=subprocess.PIPE,
                                 encoding='utf-8')
            print(f"  {out.stdout.strip()}")
            print()
        else:
            print("You may start your developer pipeline with:")
            print(f"  oc start-build {pipeline_name}")
            print()
    else:
        assert args.delete_devel

        delete_developer_resources(args, resources)


def targeting_official_namespace():
    ctx_name = subprocess.check_output(['oc', 'config', 'current-context'],
                                       encoding='utf-8').strip()
    cfg = yaml.safe_load(subprocess.check_output(['oc', 'config', 'view'],
                                                 encoding='utf-8'))
    ctx = [c['context'] for c in cfg['contexts'] if c['name'] == ctx_name]
    assert len(ctx) == 1, f"Found {len(ctx)} contexts named '{ctx_name}'"
    ctx = ctx[0]

    cluster_name = ctx['cluster']
    namespace = ctx['namespace']
    url = [c['cluster']['server'] for c in cfg['clusters']
           if c['name'] == cluster_name]
    assert len(url) == 1, f"Found {len(url)} clusters named '{cluster_name}'"
    url = url[0]

    return (url == "https://api.ocp.ci.centos.org:6443" and
            namespace == 'fedora-coreos')


def parse_args():
    parser = argparse.ArgumentParser()
    action = parser.add_mutually_exclusive_group(required=True)
    action.add_argument("--update", action='store_true',
                        help="Create or update resources")
    action.add_argument("--delete-devel", action='store_true',
                        help="Delete developer resources")
    parser.add_argument("--official", action='store_true',
                        help="Whether to update official resources "
                             "(implies --all)")
    parser.add_argument("--all", action='store_true',
                        help="Whether to update all resources")
    parser.add_argument("--dry-run", action='store_true',
                        help="Only print what would happen")
    parser.add_argument("--prefix", help="Developer prefix for resources",
                        default=get_username())
    parser.add_argument("--start", action='store_true',
                        help="Start pipeline after updating it")
    parser.add_argument("--pipeline", metavar='<URL>[@REF]',
                        help="Repo and ref to use for pipeline code")
    parser.add_argument("--config", metavar='<URL>[@REF]',
                        help="Repo and ref to use for FCOS config")
    parser.add_argument("--bucket", metavar='BUCKET',
                        help="AWS S3 bucket to use")
    parser.add_argument("--gcp-gs-bucket", metavar='GCP_GS_BUCKET',
                        help="GCP GS bucket to use for image uploads during import")
    parser.add_argument("--cosa-img", metavar='FQIN',
                        help="Pullspec to use for COSA image")
    parser.add_argument("--pvc-size", metavar='SIZE',
                        help="Size of PVC to create for cache")
    # XXX: to add as a mutually exclusive option with above
    # parser.add_argument("--cosa", metavar='<URL>[@REF]',
    #                     help="Repo and ref to use for COSA image",
    #                     default=DEFAULT_COSA_IMG)

    args = parser.parse_args()

    if args.official:
        args.all = True

    # just sanity check we have a prefix
    assert len(args.prefix)
    assert not args.prefix.endswith('-'), "Prefix must not end with dash"

    # e.g. jlebon --> jlebon-fedora-coreos-pipeline
    args.prefix += '-'

    return args


def get_username():
    import pwd
    return pwd.getpwuid(os.getuid()).pw_name


def process_template(args):
    params = {}
    if not args.official:
        params['DEVELOPER_PREFIX'] = args.prefix
    if args.pipeline:
        params.update(params_from_git_refspec(args.pipeline, 'JENKINS_S2I'))
        params.update(params_from_git_refspec(args.pipeline, 'JENKINS_JOBS'))
    if args.config:
        params.update(params_from_git_refspec(args.config, 'FCOS_CONFIG'))
    if args.bucket:
        params['S3_BUCKET'] = args.bucket
    if args.cosa_img:
        params['COREOS_ASSEMBLER_IMAGE'] = args.cosa_img
    if args.pvc_size:
        params['PVC_SIZE'] = args.pvc_size
    if args.gcp_gs_bucket:
        params['GCP_GS_BUCKET'] = args.gcp_gs_bucket

    print("Parameters:")
    for k, v in params.items():
        print(f"  {k}={v}")
    print()

    def gen_param_args(selected):
        selected_params = {(k, v) for k, v in params.items() if k in selected}
        return [q for k, v in selected_params for q in ['--param', f'{k}={v}']]

    resources = []
    for template in ['pipeline.yaml', 'jenkins-s2i.yaml']:
        # we only want to pass the params which each template actually
        # supports, so filter it down for this specific template
        with open(f'manifests/{template}') as f:
            t = yaml.safe_load(f)
            tparams = [p['name'] for p in t.get('parameters', [])]
        # and generate the --param FOO=bar ... args for this template
        param_args = gen_param_args(tparams)
        j = json.loads(subprocess.check_output(
            ['oc', 'process', '--filename', f'manifests/{template}'] + param_args))
        resources += j['items']
    return resources


def update_resources(args, resources):
    print("Updating:")
    for resource in resources:
        if args.all or is_default_resource(resource):
            action = resource_action(resource)
            if action == 'skip':
                continue
            if args.dry_run:
                print(f"Would {action} {resource['kind'].lower()} {resource['metadata']['name']}")
                continue
            out = subprocess.run(['oc', action, '--filename', '-'],
                                 input=json.dumps(resource), encoding='utf-8',
                                 check=True, stdout=subprocess.PIPE)
            print(f"  {out.stdout.strip()}")
    print()


def is_default_resource(resource):
    annos = resource['metadata'].get('annotations', {})
    return annos.get('coreos.com/deploy-default') == "true"


def resource_action(resource):
    if resource_exists(resource):
        # Some resources don't support being updated post-creation; let's just
        # skip those for now if they already exist.
        kind = resource['kind'].lower()
        if kind in ['persistentvolumeclaim']:
            print(f"  {kind} \"{resource['metadata']['name']}\" skipped")
            return 'skip'
        return 'replace'
    return 'create'


def resource_exists(resource):
    return subprocess.call(['oc', 'get', resource['kind'],
                            resource['metadata']['name']],
                           stdout=subprocess.DEVNULL,
                           stderr=subprocess.DEVNULL) == 0


def delete_developer_resources(args, resources):
    # only delete prefixed resources
    print("Deleting:")
    for resource in resources:
        if resource['metadata']['name'].startswith(args.prefix):
            out = subprocess.run(['oc', 'delete', '--filename', '-'],
                                 input=json.dumps(resource), encoding='utf-8',
                                 check=True, stdout=subprocess.PIPE)
            print(f"  {out.stdout.strip()}")
    print()


def params_from_git_refspec(refspec, param_prefix):
    url, ref = parse_git_refspec(refspec)
    return {f'{param_prefix}_URL': url,
            f'{param_prefix}_REF': ref}


def parse_git_refspec(refspec):
    if '@' not in refspec:
        return (refspec, get_default_branch(refspec))
    return tuple(refspec.split('@'))


def get_default_branch(repo):
    output = subprocess.check_output(['git', 'ls-remote', '--symref',
                                      repo, 'HEAD'],
                                     encoding='utf-8')
    for line in output.splitlines():
        if line.startswith('ref: '):
            ref, symref = line[len('ref: '):].split()
            if symref != "HEAD":
                continue
            assert ref.startswith("refs/heads/")
            return ref[len("refs/heads/"):]


def eprint(*args):
    print(*args, file=sys.stderr)


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