import inspect
import os
import re
import sys
import json
from builtins import object
from operator import attrgetter
from os import sep, path, mkdir
from threadlocal_aws.clients import codebuild
from n_utils.aws_infra_util import yaml_to_dict, import_scripts, yaml_save
from cloudformation_utils.tools import cloudformation_yaml_loads as yaml_loads
try:
    from os import scandir
except ImportError:
    from scandir import scandir

from n_utils.git_utils import Git
from n_utils.aws_infra_util import load_parameters

class Component(object):
    subcomponent_classes = []
    def __init__(self, name, project):
        self.name = name
        self.subcomponents = []
        self.project = project
        if not self.subcomponent_classes:
            self.subcomponent_classes = [name_and_obj for name_and_obj in inspect.getmembers(sys.modules["n_utils.ndt_project"]) if name_and_obj[0].startswith("SC") and inspect.isclass(name_and_obj[1])]
    
    def get_subcomponents(self):
        if not self.subcomponents:
            self.subcomponents = sorted(self._find_subcomponents(), key=attrgetter("name"))
        return self.subcomponents
    
    def _find_subcomponents(self):
        ret = []
        for subdir in [de.name for de in scandir(self.project.root + sep + self.name) if self._is_subcomponent(de.name)]:
            for _, obj in self.subcomponent_classes:
                if obj(self, "").match_dirname(subdir):
                    if subdir == "image":
                        sc_name = ""
                    else:
                        sc_name = "-".join(subdir.split("-")[1:])
                    ret.append(obj(self, sc_name))
        return ret

    def _is_subcomponent(self, dir):
        for _, obj in self.subcomponent_classes:
            if obj(self, "").match_dirname(dir):
                return True
        return False

class SubComponent(object):
    def __init__(self, component, name):
        self.component = component
        self.name = name
        self.type = self.__class__.__name__[2:].lower()

    def get_dir(self):
        return self.component.name + sep + self.type + "-" + self.name

    def match_dirname(self, dir):
        return dir.startswith(self.type + "-")

    def list_row(self, branch):
        return ":".join([self.component.name, branch, self.type, self.name])

    def job_properties_filename(self, branch, root):
        name_arr = [self.type, re.sub(r'[^\w-]', '_', branch), self.component.name, self.name]
        return root + sep + "job-properties" + sep + "-".join(name_arr) + ".properties"

class SCImage(SubComponent):
    def get_dir(self):
        if self.name:
            return self.component.name + sep + "image-" + self.name
        else:
            return self.component.name + sep + "image"

    def match_dirname(self, dir):
        return dir == "image" or dir.startswith("image-")

    def list_row(self, branch):
        if not self.name:
            name = "-"
        else:
            name = self.name
        return ":".join([self.component.name, branch, self.type, name])

    def job_properties_filename(self, branch, root):
        name_arr = [self.type, re.sub(r'[^\w-]', '_', branch), self.component.name]
        if self.name:
            name_arr.append(self.name)
        return root + sep + "job-properties" + sep + "-".join(name_arr) + ".properties"

class SCStack(SubComponent):
    pass

class SCDocker(SubComponent):
    pass

class SCServerless(SubComponent):
    pass

class SCCDK(SubComponent):
    pass

class SCTerraform(SubComponent):
    pass



class Project(object):
    def __init__(self, root=".", branch=None):
        if not branch:
            self.branch = Git().get_current_branch()
        else:
            self.branch = branch
        self.componets = []
        self.root = root if root else guess_project_root()
        self.all_subcomponents = []

    def get_components(self):
        if not self.componets:
            self.componets = sorted(self._find_components(), key=attrgetter("name"))
        return self.componets

    def get_component(self, component):
        filtered = [c for c in self.get_components() if c.name == component]
        if len(filtered) == 1:
            return filtered[0]
        return None

    def _find_components(self):
        return [Component(de.name, self) for de in scandir(self.root) if de.is_dir() and self._is_component(de.path)]

    def get_all_subcomponents(self, sc_type=None):
        if not self.all_subcomponents:
            for component in self.get_components():
                self.all_subcomponents.extend(component.get_subcomponents())
        if not sc_type:
            return self.all_subcomponents
        else:
            return [sc for sc in self.all_subcomponents if sc.type == sc_type]

    def _is_component(self, dir):
        return len([de for de in scandir(dir) if de.is_file() and (de.name == "infra.properties" or (de.name.startswith("infra-") and de.name.endswith(".properties")))]) > 0

def guess_project_root():
    
    for guess in [".", Git().get_git_root(), "..", "../..", "../../..", "../../../.."]:
        if len(Project(root=guess).get_all_subcomponents()) > 0:
            if guess == ".":
                return guess
            else:
                return path.abspath(guess)

def list_jobs(export_job_properties=False, branch=None, json=False, component=None):
    ret = {"branches":[]}
    arr = []
    param_files = {}
    with Git() as git:
        current_project = Project(root=guess_project_root())
        if branch:
            branches = [ branch ]
        else:
            branches = git.get_branches()
        components = []
        for c_branch in branches:
            branch_obj = {"name": c_branch, "components": []}
            ret["branches"].append(branch_obj)
            if c_branch == git.get_current_branch():
                project = current_project
            else:
                root = git.export_branch(c_branch)
                project = Project(root=root, branch=c_branch)
            if component:
                c_component = project.get_component(component)
                if not c_component:
                    print("No matching components")
                    if json:
                        return {}
                    else:
                        return []
                branch_obj["components"].append({"name": c_component.name, "subcomponents": []})
                components.append(c_component)
            else:
                for c_component in project.get_components():
                    branch_obj["components"].append({"name": c_component.name, "subcomponents": []})
                    components.append(c_component)
        if not json and export_job_properties:
            try:
                mkdir(current_project.root + sep + "job-properties")
            except OSError as err:
                # Directory already exists is ok
                if err.errno == 17:
                    pass
                else:
                    raise err
        if json:
            _collect_json(components, ret, export_job_properties ,git)
        else:
            arr, param_files = _collect_prop_files(components, export_job_properties, current_project.root, git)
            if export_job_properties:
                _write_prop_files(param_files)
    if json:
        return ret
    else:
        return arr

def _collect_json(components, ret, export_job_properties, git):
    with git:
        for component in components:
            subcomponents = component.get_subcomponents()
            for subcomponent in subcomponents:
                branch_elem = [b for b in ret["branches"] if b["name"] == component.project.branch][0]
                component_elem = [c for c in branch_elem["components"] if c["name"] == component.name][0]
                subc_elem = {"type": subcomponent.type}
                if subcomponent.name:
                    subc_elem["name"] = subcomponent.name
                component_elem["subcomponents"].append(subc_elem)
                if export_job_properties:
                    prop_args = {
                        "component": subcomponent.component.name,
                        subcomponent.type: subcomponent.name,
                        "branch": component.project.branch,
                        "git": git
                    }
                    subc_elem["properties"] = load_parameters(**prop_args)

def _collect_prop_files(components, export_job_properties, root, git):
    arr = []
    param_files = {}
    with git:
        for component in components:
            subcomponents = component.get_subcomponents()
            for subcomponent in subcomponents:
                arr.append(subcomponent.list_row(component.project.branch))
                if export_job_properties:
                    #$TYPE-$GIT_BRANCH-$COMPONENT-$NAME.properties
                    filename = subcomponent.job_properties_filename(component.project.branch, root)
                    prop_args = {
                        "component": subcomponent.component.name,
                        subcomponent.type: subcomponent.name,
                        "branch": component.project.branch,
                        "git": git
                    }
                    parameters = load_parameters(**prop_args)
                    param_files[filename] = parameters
    return arr, param_files

def _write_prop_files(param_files):
    for filename, parameters in list(param_files.items()):
        with open(filename, 'w+') as prop_file:
            for key, value in list(parameters.items()):
                prop_file.write(key + "=" + value + "\n")

def list_components(branch=None, json=None):
    return [c.name for c in Project(branch=branch).get_components()]

DEFAULT_BUILD_SPEC = \
"""version: 0.2

phases:
    build:
        commands:
            - echo $CODEBUILD_SOURCE_VERSION
            - ndt ${command} ${component} ${subcomponent}
"""
template = """{
    "name": "",
    "source": {
        "type": "{clone_type}",
        "location": "{clone_url}",
        "gitCloneDepth": 1,
        "gitSubmodulesConfig": {
            "fetchSubmodules": false
        },
        "buildspec": "",
        "reportBuildStatus": false,
        "insecureSsl": false
    },
    "artifacts": {
        "type": "NO_ARTIFACTS"
    },
    "cache": {
        "type": "NO_CACHE"
    },
    "environment": {
        "type": "LINUX_CONTAINER",
        "image": "nitor/ndt:1.138",
        "computeType": "BUILD_GENERAL1_SMALL",
        "environmentVariables": [],
        "privilegedMode": true,
        "imagePullCredentialsType": "SERVICE_ROLE"
    },
    "serviceRole": "{service_role}",
    "timeoutInMinutes": 60,
    "queuedTimeoutInMinutes": 480,
    "logsConfig": {
        "cloudWatchLogs": {
            "status": "ENABLED"
        },
        "s3Logs": {
            "status": "DISABLED",
            "encryptionDisabled": false
        }
    }
}"""
webhook_template = """{
        "filterGroups": [
            [
                {
                    "type": "EVENT",
                    "pattern": "PULL_REQUEST_MERGED",
                    "excludeMatchedPattern": false
                },
                {
                    "type": "FILE_PATH",
                    "pattern": "",
                    "excludeMatchedPattern": false
                },
                {
                    "type": "BASE_REF",
                    "pattern": "",
                    "excludeMatchedPattern": false
                }
            ]
        ]
}"""

def upsert_codebuild_projects(dry_run=False):
    template_args = json.loads(template)
    webhook_args = json.loads(webhook_template)
    branch = branch = Git().get_current_branch()
    print(f"Listing jobs in {branch}")
    jobs = list_jobs(export_job_properties=True, branch=branch, json=True)
    template_args["environment"]["environmentVariables"].append({
        "name": "GIT_BRANCH",
        "value": branch,
        "type": "PLAINTEXT"
    })
    template_args["sourceVersion"] = branch
    for component in jobs["branches"][0]["components"]:
        component_name = component["name"]
        for subcomponent in component["subcomponents"]:
            component_args = template_args.copy()
            subcomponent_type = subcomponent["type"]
            command = "deploy-" + subcomponent_type
            if subcomponent_type in [ "docker", "image" ]:
                command = "bake-" + subcomponent_type
            subcomponent_name = subcomponent["name"]
            orig_name_param = "ORIG_" + subcomponent_type.upper() + "_NAME"
            if orig_name_param in subcomponent["properties"]:
                subcomponent_dir = component_name + "/" + subcomponent_type + "-" + subcomponent["properties"][orig_name_param]
            else:
                subcomponent_dir = component_name + "/" + subcomponent_type
            if "BUILD_JOB_NAME" in subcomponent["properties"]:
                component_args["name"] = subcomponent["properties"]["BUILD_JOB_NAME"]
            else:
                component_args["name"] = f"{subcomponent['properties']['BUILD_JOB_PREFIX']}-{component_name}-{command.split('-')[0]}-{subcomponent_name}"
            if "CODEBUILD_SERVICE_ROLE" in subcomponent["properties"]:
                component_args["serviceRole"] = subcomponent["properties"]["CODEBUILD_SERVICE_ROLE"]
            else:
                print(f"CODEBUILD_SERVICE_ROLE needs to be defined, skipping {component_name}/{subcomponent_name}")
                continue
            if "CODEBUILD_SOURCE_TYPE" in  subcomponent["properties"]:
                component_args["source"]["type"] = subcomponent["properties"]["CODEBUILD_SOURCE_TYPE"]
                if "CODEBUILD_SOURCE_LOCATION" in subcomponent["properties"]:
                    component_args["source"]["location"] = subcomponent["properties"]["CODEBUILD_SOURCE_LOCATION"]
                else:
                    del component_args["source"]
            else:
                del component_args["source"]
            extra_params = {"component": component_name, "command": command, "subcomponent": subcomponent_name}
            if "source" in component_args:
                if "BUILD_SPEC" in  subcomponent["properties"]:
                    build_spec = subcomponent["properties"]["BUILD_SPEC"]
                else:
                    build_spec = DEFAULT_BUILD_SPEC
                try:
                    interpolated_build_spec = yaml_to_dict(build_spec, extra_parameters=extra_params)
                except:
                    interpolated_build_spec = yaml_loads(build_spec)
                    interpolated_build_spec = import_scripts(interpolated_build_spec, subcomponent_dir + os.sep + "infra.properties", extra_parameters=extra_params)
                component_args["source"]["buildspec"] = yaml_save(interpolated_build_spec)
            # Make sure that builds that need docker, start docker
            if "NEEDS_DOCKER" in subcomponent["properties"] and subcomponent["properties"]["NEEDS_DOCKER"] == "y" and "phases" in interpolated_build_spec:
                start_docker_found = False
                for phase in interpolated_build_spec["phases"]:
                    if start_docker_found:
                        break
                    if "commands" in phase:
                        for command in phase["commands"]:
                            if command.strip() == "start-docker.sh":
                                start_docker_found = True
                                break
                if not start_docker_found:
                    phase = interpolated_build_spec["phases"]["build"]
                    if "pre_build" in interpolated_build_spec["phases"]:
                        phase = interpolated_build_spec["phases"]["pre_build"]
                    if "commands" not in phase:
                        phase["commands"] = []
                    phase["commands"].insert(0, "start-docker.sh")
            for flter in webhook_args["filterGroups"][0]:
                if flter["type"] == "FILE_PATH":
                    flter["pattern"] = subcomponent_dir + "/.*"
                if flter["type"] == "BASE_REF":
                    flter["pattern"] = f"^refs/heads/{branch}$"
            webhook_args["projectName"] = component_args['name']
            print(f"Updating {component_args['name']}")
            if dry_run:
                print(json.dumps(component_args, indent=2))
                print(json.dumps(webhook_args, indent=2))
            else:
                try:
                    codebuild().update_project(**component_args)
                    codebuild().update_webhook(**webhook_args)
                except:
                    print(f"Project not found, creating {component_args['name']}")
                    codebuild().create_project(**component_args)
                    print(f"Creating webhook for {component_args['name']}")
                    codebuild().create_webhook(**webhook_args)
