import sys
import requests
import yaml
import threading
import os
from ast import literal_eval
from dataclasses import dataclass
from typing import Dict
from time import sleep
from datetime import datetime, timedelta
import update_docker_tag
import update_ecs_service
from github.github_apis import get_default_branch
from actions_logging.app_logging import logger


time_diff_minutes = 180
git_token = os.getenv("GLOBAL_CICD_GIT_TOKEN")
git_headers = {
    'Accept': 'application/vnd.github.v3+json',
    'Authorization': f'token {git_token}',
    'Content-Type': 'application/x-www-form-urlencoded',
}
git_org = "perimeter-81"
github_url = f"https://api.github.com/repos/{git_org}"
git_user_admin = 'sayuser'
time_format = "%Y-%m-%dT%H:%M:%SZ"
stop_execute = False
gha_workflows = {
    "core-ecs": {
        "workflow_file": "build_and_deploy_or_just_deploy.yaml",
        "version_key_name": "CONTAINER_TO_DEPLOY",
        "extra_data": {
            "BUILD_FROM_SCRATCH": "false"
        }
    },
    "core-manifest": {
        "workflow_file": "create_and_deploy_manifest.yaml",
        "version_key_name": "GIT_VERSION"
    },
    "frontend": {
        "workflow_file": "webclient.yaml",  # TODO make it dynamic for all frontend
        "version_key_name": "VERSION_TO_DEPLOY",
        "extra_data": {
            "BUILD_FROM_SCRATCH": "false"
        }
    }
}


@dataclass
class MsData:
    ms_name: str
    version: str
    type: str
    stage: str
    repo_name: str
    branch: str


def generate_workflow_inputs(env, ms: MsData):
    inputs = {
        "ENV_NAME": env,
        gha_workflows[ms.type]["version_key_name"]: ms.version
    }
    if gha_workflows[ms.type].get("extra_data"):
        inputs.update(gha_workflows[ms.type]["extra_data"])
    return inputs


def get_repos():
    repos = {}
    with open("repos.yaml", "r") as stream:
        # with open("repos.yaml", "r") as stream:  # TODO for tests
        try:
            repos = yaml.safe_load(stream)
        except yaml.YAMLError as e:
            logger.error(e)
    return repos


def invert_dict(d):
    # Create a new dictionary that will hold the inverted key-value pairs
    inverted = {}
    # Loop through all the items in the original dictionary
    for key, value in d.items():
        if isinstance(value, list):
            for ms_dict in value:
                inverted[ms_dict['ms_name']] = key

        # Add the inverted key-value pair to the new dictionary
        else:
            inverted[value['ms_name']] = key
    # Return the new inverted dictionary
    return inverted


def get_correct_run_id(repo_name, workflow_id):
    two_minutes_ago = datetime.now() - timedelta(minutes=1)
    date_format = datetime.strftime(two_minutes_ago, time_format)
    logger.info(date_format)
    now_format = datetime.strftime(datetime.now() + timedelta(minutes=2), time_format)
    logger.info(f"now - {now_format}")
    params = {
        "per_page": 100,
        "created": f"{date_format}..{now_format}"
    }
    logger.info(f"params- {params}")
    sys.stdout.flush()
    sleep(30)
    res = requests.get(f"{github_url}/{repo_name}/actions/workflows/{workflow_id}/runs",
                       headers=git_headers,
                       params=params)
    if res.json().get('total_count'):
        if res.json()['total_count'] != 1:
            logger.error("error in workflow runs:")
            logger.error(res.json())
    if res.status_code > 299:
        logger.error("ERROR:")
        logger.error(res.json())
        return
    for run in res.json()['workflow_runs']:
        logger.info(f"run status: {run['status']}")
        if run['status'] == "waiting":
            run_id = run['id']
            logger.info(f"run id: {run_id}")
            return run_id
    logger.error("ERROR- no runs and with the right status. the response::")
    logger.error(res.json())
    return None


def get_deployment_id(repo_name, run_id):
    res = requests.get(f"{github_url}/{repo_name}/actions/runs/{run_id}/pending_deployments", headers=git_headers)
    res = res.json()
    # logger.error(res)
    return res[0]['environment']['id']


def approve_deployment(repo_name, run_id, env_id):
    data = {
        "environment_ids": [env_id],
        "state": "approved",
        "comment": "deployment approved by sayuser as part of release-app"
    }
    res = requests.post(f"{github_url}/{repo_name}/actions/runs/{run_id}/pending_deployments",
                        headers=git_headers,
                        json=data)
    if res.status_code > 299:
        logger.error("ERROR in approve deployment")
        logger.error(res.json())
    else:
        logger.info("deployment approved successfully")


def wait_for_run(repo_name, workflow_id, ms_name) -> bool:
    global stop_execute
    run_id = get_correct_run_id(repo_name, workflow_id)
    env_id = get_deployment_id(repo_name, run_id)
    if not env_id:
        logger.error("ERROR- no deployment id")
        return False
    approve_deployment(repo_name, run_id, env_id)
    for _ in range(250):
        resp = requests.get(f"{github_url}/{repo_name}/actions/runs/{run_id}", headers=git_headers)
        run_data = resp.json()
        status = run_data['status']
        conclusion = run_data['conclusion']
        if status == "completed":
            if conclusion == "success":
                return True
            else:
                logger.error(f"service {ms_name} in status: {status} conclusion: {conclusion}")
                return False
        logger.info(f"service {ms_name} in status: {status} conclusion: {conclusion}")
        sys.stdout.flush()
        sleep(10)
    logger.info(f"service {ms_name} in status: {status} conclusion: {conclusion} - timeout")
    return False


def run_workflow(repo_name, workflow_id, branch, inputs, ms_name):
    global stop_execute
    data = {
        "ref": branch,
        "inputs": inputs
    }
    logger.info(data)
    logger.info(workflow_id)
    response = requests.post(
        f'{github_url}/{repo_name}/actions/workflows/{workflow_id}/dispatches',
        headers=git_headers, json=data)
    if response.status_code == 204:
        logger.info("workflow run command send successfully")
        run_status = wait_for_run(repo_name, workflow_id, ms_name)
        if run_status:
            logger.info(f"ya-ba-da-ba-du!!!! service {ms_name} deployed successfully")
        else:
            stop_execute = True
            logger.error(f"error in service {ms_name}. failed")
    else:
        logger.error(f"error in running another workflow: for repo {repo_name} ")
        logger.error(response.status_code)
        logger.error(response.json())
        stop_execute = True


def promote_ecs(ms_name, env_name, ms_version):
    res = update_docker_tag.main(ms_name, env_name, ms_version)
    if res:
        update_ecs_service.update_service(ms_name, env_name)


def promote(ms_data: MsData):
    version = None
    if not invert_repos.get(ms_data.ms_name):
        logger.error(f"in get_env_version ms_name: {ms_data.ms_name} not in repos")
        return version
    ms_type = invert_repos[ms_data.ms_name]['type']
    if ms_type == 'backend':
        if types == 'all' or ms_type == types:
            promote_ecs(ms_data.ms_name, env_name, ms_data.version)
    elif ms_type == 'core-ecs':
        if types == 'all' or ms_type == types:
            promote_ecs(ms_data.ms_name, env_name, ms_data.version)
    elif ms_type == 'frontend':
        if types == 'all' or ms_type == types:
            inputs = generate_workflow_inputs(env_name, ms_data)
            run_workflow(
                ms_data.repo_name,
                gha_workflows[ms_data.type]["workflow_file"],
                "DevOps",  # ms_data.branch,
                inputs,
                ms_data.ms_name
            )
    # elif ms_type == 'lambda':
    #     if types == 'all' or ms_type == types:
    #         version = get_lambda_version(ms_data.ms_name, env_name)
    # elif ms_type == 'pipeline':
    #     if types == 'all' or ms_type == types:
    #         version = get_pipeline_version(ms_data.ms_name, env_name)
    else:
        logger.error(f"in get_env_version ms_name: {ms_data.ms_name} type : {ms_type}")
        logger.error(f"type not in ms to deploy")
    return version


def run_stage(stage):
    global stop_execute
    ms_dict: Dict[str, MsData] = stages[stage]
    threads = []
    logger.info(f"start run stage {stage}")
    for ms_data in ms_dict.values():
        t = threading.Thread(target=promote, args=[ms_data])
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    if stop_execute:
        logger.error(f"stop the execution because stage {stage} did not finish properly")
        exit(1)
    logger.info(f"finish run stage {stage}")


def main():
    for ms_name, ms_data in new_inputs.items():
        stage = ms_data['stage']
        repo_name = invert_repos[ms_name]
        default_branch = get_default_branch(repo_name)
        ms = MsData(ms_name, ms_data['version'], ms_data['type'], stage, repo_name, default_branch)
        if stage not in stages:
            stages[stage] = {ms_name: ms}
        else:
            stages[stage][ms_name] = ms
    stages_list = sorted(stages.keys())
    logger.info(stages_list)

    for stage_num in stages_list:
        run_stage(stage_num)


if __name__ == '__main__':
    repos = get_repos()
    invert_repos = invert_dict(repos)
    stages = {}
    env_name = os.getenv('ENV_NAME')
    types = sys.argv[1].replace("types=", "", 1)
    inputs = os.getenv('INPUTS')
    new_inputs = literal_eval(inputs)
    logger.info(inputs)
    main()
