#!/usr/bin/env python3
import copy
import json
from io import StringIO
from pathlib import Path
from typing import Optional

from jsonschema import validate
from ruamel.yaml import YAML as _YAML
from ruamel.yaml.scalarstring import DoubleQuotedScalarString
from termcolor import colored

from ..constants import SCHEMA_DIR


class YAML(_YAML):
    def dumps(self, data, **kw):
        stream = StringIO()
        _YAML.dump(self, data, stream, **kw)
        return stream.getvalue()


def merge_dict(orig: dict, new: dict) -> dict:
    final = {}

    all_keys = sorted(list(set(orig.keys()) | set(new.keys())))
    for key in all_keys:
        if key in orig and key in new:
            if isinstance(orig[key], dict):
                if not isinstance(new[key], dict):
                    raise NotImplementedError("Can't merge dict and non-dict keys")
                final[key] = merge_dict(orig[key], new[key])
            else:
                final[key] = new[key]  # new takes precedence
        elif key in orig:
            final[key] = orig[key]
        elif key in new:
            final[key] = new[key]
        else:
            raise NotImplementedError("This should not possible")
    return final


def simplify_job(data, job):
    job = copy.deepcopy(job)

    if not "extends" in job:
        return job

    extends = job["extends"]
    if isinstance(extends, str):
        extends = [extends]

    new_job = {}
    for extend in extends:
        new_job = merge_dict(new_job, simplify_job(data, data[extend]))
    new_job = merge_dict(new_job, job)
    del new_job["extends"]
    new_job = {key: value for key, value in sorted(new_job.items())}

    return new_job


def get_global_variables(data, name):
    if not "variables" in data:
        return
    variable_prefix = f"{name.upper()}_"
    variables = {}
    for key, value in data["variables"].items():
        assert key.startswith(variable_prefix)
        variables[key] = value
    return variables


def straight_copy(data, job_data, key):
    if key not in data:
        return
    job_data[key] = copy.deepcopy(data[key])


def add_hidden_pattern(pattern):
    return tuple([f".{pattern}", pattern])


def get_job_prefix(
    pipeline_name: str, job_name: str, include_hidden_jobs: bool = False
) -> Optional[str]:
    if job_name.startswith("."):
        if not include_hidden_jobs:
            return None
        return f".{pipeline_name}-"
    return f"{pipeline_name}-"


def fix_string_format(data):
    if "variables" in data:
        for key, value in data["variables"].items():
            if "\n" not in value:
                data["variables"][key] = DoubleQuotedScalarString(value)


def fix_script_anchors(data):
    for key in data:
        if key not in (
            "before_script",
            "script",
            "after_script",
        ):
            continue
        if isinstance(data[key], list):
            newdata = []
            for item in data[key]:
                if isinstance(item, list):
                    newdata.extend(item)
                else:
                    newdata.append(item)
            data[key] = newdata


def write_ci_file(output_path: Path, group_name: str, group_data: dict):
    output_file = output_path / f"{group_name}.yml"

    yaml = YAML()
    yaml.default_flow_style = False
    yaml.explicit_start = False
    yaml.preserve_quotes = True
    yaml.indent(mapping=2, sequence=4, offset=2)

    fix_string_format(group_data)
    for job_data in group_data.values():
        fix_string_format(job_data)
        fix_script_anchors(job_data)

    with open(output_file, "w") as handle:
        print(
            colored("writing include file:", color="magenta"),
            output_file.relative_to(Path.cwd()),
        )
        handle.write("# this CI file was generated by cici; do not hand-edit\n")
        blocks = [
            yaml.dumps({key: group_data[key]}).rstrip() for key in sorted(group_data)
        ]
        handle.write("\n\n".join(blocks) + "\n")


def bundle_command(args):
    config_file = args.config_path / "config.yml"
    gitlab_file = args.config_path / "gitlab" / "main.yml"

    args.output_path = Path(args.output_path)
    if args.groups:
        args.groups = [group.strip() for group in args.groups.split(",")]

    yaml = YAML()
    if config_file.exists():
        config_data = yaml.load(open(config_file).read())
        if config_data is not None and "groups" in config_data:
            if not args.groups:
                args.groups = config_data["groups"]

    text = open(gitlab_file).read()
    data = yaml.load(text)

    schema_file = SCHEMA_DIR / "gitlab-ci.json"

    schema = json.load(open(schema_file))
    validate(data, schema=schema)

    # get reserved words
    keywords = {key for key in schema["properties"].keys() if key not in ("pages",)}

    # get jobs
    jobs = {key: value for key, value in data.items() if key not in keywords}

    if not args.groups:
        group_names = set()
        for job_name in jobs.keys():
            if job_name.startswith("."):
                continue
            group_name = job_name[len(f"{args.pipeline_name}-") :]
            group_name = group_name.split("-")[0]
            group_names.add(group_name)
        args.groups = sorted(list(group_names))

    print(colored("job groups:", color="yellow"), args.groups)

    bundled_jobs = {}

    for jobname in jobs:
        job_prefix = get_job_prefix(
            pipeline_name=args.pipeline_name,
            job_name=jobname,
            include_hidden_jobs=args.include_hidden_jobs,
        )
        if not job_prefix:
            continue
        assert jobname.startswith(job_prefix)

        new_job = simplify_job(data, jobs[jobname])

        job_data = {jobname: new_job}
        bundled_jobs[jobname] = job_data

    # organize jobs into groups
    groups = {}
    for group_name in args.groups:
        if group_name.startswith("."):
            pattern = f".{args.pipeline_name}-{group_name[1:]}"
        else:
            pattern = f"{args.pipeline_name}-{group_name}"

        for job_name, job_data in bundled_jobs.items():
            if job_name.startswith(pattern):
                groups.setdefault(group_name, {})
                groups[group_name].update(job_data)

        if group_name in groups:
            groups[group_name]["variables"] = get_global_variables(
                data, args.pipeline_name
            )
            straight_copy(data, groups[group_name], "stages")
            straight_copy(data, groups[group_name], "workflow")

    for group_name, group_data in groups.items():
        validate(group_data, schema=schema)
        write_ci_file(args.output_path, group_name=group_name, group_data=group_data)
