import json
import time
from datetime import datetime, timedelta

import requests
from colorama import Fore

from src.enum.project_type import ProjectTypeEnum

from . import exit_message, get_env_variable_required, print_message

PROJECT_URL = "/project/"
BOM_URL = "/bom/"
VIOLATION_URL = "/violation/"
EVENT_URL = "/event/"

PROJECT_CLASSIFIER_APPLICATION = "APPLICATION"
PROJECT_CLASSIFIER_LIBRARY = "LIBRARY"

DEFAULT_TIMEOUT = 60.0
RETRY_BACKOFF_FACTOR = 5
DEFAULT_MAX_RETRY_TIMEOUT = 300.0


class DependencyTrackHelper(object):
    def __init__(self):
        self.ci_project_namespace = get_env_variable_required("CI_PROJECT_NAMESPACE")
        self.ci_project_name = get_env_variable_required("CI_PROJECT_NAME")
        self.ci_project_url = get_env_variable_required("CI_PROJECT_URL")
        self.base_url = get_env_variable_required("SCI_DEPENDENCY_TRACK_URL")
        self.base_api_url = f"{self.base_url}/api/v1"
        self.api_key = get_env_variable_required("SCI_DEPENDENCY_TRACK_API_KEY")
        self.current_branch = get_env_variable_required("CI_COMMIT_REF_NAME")
        self.default_branch = "master"
        self.session = requests.Session()
        self.session.headers.update({"X-API-Key": self.api_key})
        raw_project_type = get_env_variable_required("SCI_PROJECT_TYPE")
        self.sci_project_type = ProjectTypeEnum[raw_project_type]

    def _post(self, url, data):
        return self.session.post(f"{self.base_api_url}{url}", json=data, timeout=30.0)

    def _post_form_data(self, url, data, files):
        return self.session.post(
            f"{self.base_api_url}{url}",
            data=data,
            files=files,
            timeout=60.0,
        )

    def _get(self, url, params=None, timeout=30.0):
        return self.session.get(
            f"{self.base_api_url}{url}", params=params, timeout=timeout
        )

    def _put(self, url, data):
        return self.session.put(f"{self.base_api_url}{url}", json=data, timeout=60.0)

    def _patch(self, url, data):
        return self.session.patch(f"{self.base_api_url}{url}", json=data, timeout=30.0)

    def _delete(self, url):
        return self.session.delete(f"{self.base_api_url}{url}", timeout=30.0)

    def get_project(self, name=None, version=None):
        project_name = name or f"{self.ci_project_namespace}/{self.ci_project_name}"
        params = {"name": project_name}
        if version:
            params["version"] = version

        result = None
        response = self._get(PROJECT_URL, params=params)

        if response.status_code == 200 and response.json():
            projects = response.json()
            if version:
                for project in projects:
                    if project.get("version") == version:
                        result = project
                        break
            else:
                result = projects[0]

        return result

    def _upsert_master(self, project_name):
        project = self.get_project(project_name)
        if project and project.get("version") == self.default_branch:
            return project

        return self.create_project(name=project_name, version=self.default_branch)

    def _determine_project_classifier(self, project_type=None):
        if project_type is None:
            project_type = self.sci_project_type

        lib_types = {
            enum_type
            for enum_type in ProjectTypeEnum
            if enum_type.name.endswith("_LIB")
        }

        return (
            PROJECT_CLASSIFIER_LIBRARY
            if project_type in lib_types
            else PROJECT_CLASSIFIER_APPLICATION
        )

    def create_project(
        self,
        name=None,
        description=None,
        version=None,
    ):
        project_name = name or f"{self.ci_project_namespace}/{self.ci_project_name}"
        project_version = version or self.current_branch
        project_description = (
            description or f"Projeto importado do Gitlab: {self.ci_project_url}"
        )

        wanted_classifier = self._determine_project_classifier()
        existing_project = self.get_project(name=project_name, version=project_version)

        project_data = {
            "name": project_name,
            "version": project_version,
            "description": project_description,
            "active": True,
            "isLatest": project_version == self.default_branch,
            "classifier": wanted_classifier,
            "externalReferences": [
                {"type": "vcs", "url": self.ci_project_url},
            ],
        }

        if project_version != self.default_branch:
            master_project = self._upsert_master(project_name)
            project_data["parent"] = {"uuid": master_project["uuid"]}

        if existing_project:
            if project_version == self.default_branch:
                project_data["uuid"] = existing_project["uuid"]
                response = self._post(f"{PROJECT_URL}", project_data)
                if response.status_code != 200:
                    exit_message(
                        f'Falha ao atualizar projeto "{project_name}": {response.text}'
                    )
                return response.json()
            return existing_project

        response = self._put(PROJECT_URL, project_data)
        if response.status_code != 201:
            exit_message(f'Falha ao criar projeto "{project_name}": {response.text}')

        return response.json()

    @staticmethod
    def _execute_with_retry(
        operation,
        max_retries=3,
        initial_timeout=None,
        backoff_factor=None,
        error_message=None,
    ):
        """
        Executa uma operação com mecanismo de retry e backoff exponencial
        """
        timeout = initial_timeout or DEFAULT_TIMEOUT
        backoff = backoff_factor or RETRY_BACKOFF_FACTOR

        try:
            return operation(timeout)
        except Exception:
            pass

        for retry in range(1, max_retries):
            try:
                timeout = min(timeout + (retry * 30.0), DEFAULT_MAX_RETRY_TIMEOUT)
                time.sleep(backoff * retry)

                result = operation(timeout)
                if result is not None:
                    return result

            except Exception:
                pass

        if error_message:
            print_message(error_message, Fore.YELLOW)
        return None

    def _get_project_violations(
        self, project_uuid, timeout, page_number=1, page_size=1000
    ):
        params = {
            "pageNumber": str(page_number),
            "pageSize": str(page_size),
            "suppressed": "false",
        }

        response = self._get(
            f"{VIOLATION_URL}project/{project_uuid}", params=params, timeout=timeout
        )

        if response.status_code == 200:
            return response.json()
        return None

    @staticmethod
    def _process_violations_batch(violations):
        components_with_violations = {}

        for violation in violations:
            component = violation.get("component", {})
            component_name = component.get("name", "")
            component_version = component.get("version", "")
            component_key = f"{component_name} {component_version}"

            policy = violation.get("policyCondition", {}).get("policy", {})
            policy_name = policy.get("name", "Política não especificada")

            if component_key not in components_with_violations:
                components_with_violations[component_key] = {"policies": set()}

            components_with_violations[component_key]["policies"].add(policy_name)

        return components_with_violations

    def _get_all_project_violations(self, project_uuid, timeout, page_size=1000):
        components_with_violations = {}
        page_number = 1
        total_violations = 0

        while True:
            page_violations = self._get_project_violations(
                project_uuid, timeout, page_number=page_number, page_size=page_size
            )

            if not page_violations:
                break

            total_violations += len(page_violations)
            batch_components = self._process_violations_batch(page_violations)

            for comp_key, comp_data in batch_components.items():
                if comp_key in components_with_violations:
                    components_with_violations[comp_key]["policies"].update(
                        comp_data["policies"]
                    )
                else:
                    components_with_violations[comp_key] = comp_data

            if len(page_violations) < page_size:
                break

            page_number += 1

        return {
            "components": components_with_violations,
            "total_violations": total_violations,
        }

    def _process_violations(self, violations_data, project_uuid):
        if not violations_data or not isinstance(violations_data, dict):
            return {
                "components": {},
                "project_url": f"{self.base_url}/projects/{project_uuid}/policyViolations",
                "count": 0,
            }

        return {
            "components": violations_data["components"],
            "project_url": f"{self.base_url}/projects/{project_uuid}/policyViolations",
            "count": len(violations_data["components"]),
        }

    def retrieve_project_violations(self, project_uuid, max_retries=3, page_size=500):
        result = self._execute_with_retry(
            operation=lambda timeout: self._get_all_project_violations(
                project_uuid, timeout, page_size=page_size
            ),
            max_retries=max_retries,
            error_message="Não foi possível obter violações após múltiplas tentativas",
        )

        return result or {"components": {}, "total_violations": 0}

    @staticmethod
    def _get_bom_token(response):
        try:
            return response.json().get("token")
        except Exception:
            return None

    def wait_for_bom_processing(
        self, bom_token, timeout_seconds=60, initial_interval=3
    ):
        print(" ")
        print_message("Aguardando processamento do BOM...")

        start_time = datetime.now()
        timeout = timedelta(seconds=timeout_seconds)
        check_interval = initial_interval

        while datetime.now() - start_time < timeout:
            try:
                response = self._get(f"{EVENT_URL}token/{bom_token}")

                if response.status_code != 200:
                    exit_message(
                        f"Falha ao verificar status do processamento: {response.text}"
                    )

                status = response.json()
                processing_complete = status.get("processing") is False

                if processing_complete:
                    return True

                print(".", end="", flush=True)

                check_interval = min(check_interval * 1.5, 30)
                time.sleep(check_interval)

            except Exception as e:
                exit_message(f"Erro ao verificar status: {str(e)}")

        return False

    def upload_bom(
        self,
        bom_file_path,
        project_name=None,
        project_version=None,
    ):
        project_name = (
            project_name or f"{self.ci_project_namespace}/{self.ci_project_name}"
        )
        project_version = project_version or self.current_branch

        project = self.create_project(name=project_name, version=project_version)

        data = {
            "projectName": project_name,
            "projectVersion": project_version,
            "autoCreate": True,
        }

        if project_version != self.default_branch:
            master_project = self._upsert_master(project_name)
            data["parentUUID"] = master_project["uuid"]

        with open(bom_file_path, "rb") as bom_file:
            files = {"bom": bom_file}
            response = self._post_form_data(BOM_URL, data, files)

            if response.status_code != 200:
                exit_message(
                    f'Falha ao fazer upload do BOM para o projeto "{project_name}": {response.text}'
                )

            bom_token = self._get_bom_token(response)
            if not bom_token:
                exit_message("Não foi possível obter o token do processamento do BOM")

        return project["uuid"], bom_token

    def analyze_dependencies(
        self, bom_file_path, timeout_seconds=3000, skip_violations_for_new=True
    ):
        project_name = f"{self.ci_project_namespace}/{self.ci_project_name}"
        project_version = self.current_branch
        existing_project = self.get_project(name=project_name, version=project_version)
        is_new_project = existing_project is None

        project_uuid, bom_token = self.upload_bom(bom_file_path)

        if skip_violations_for_new and is_new_project:
            empty_report = {
                "components": {},
                "project_url": f"{self.base_url}/projects/{project_uuid}/policyViolations",
                "count": 0,
            }
            return project_uuid, empty_report

        processing_completed = self.wait_for_bom_processing(
            bom_token, timeout_seconds=timeout_seconds
        )

        if not processing_completed:
            exit_message("Timeout aguardando processamento do BOM")

        violations = self.retrieve_project_violations(project_uuid)
        violation_report = self._process_violations(violations, project_uuid)

        return project_uuid, violation_report
