import json
import os
import re
import tempfile
import xml.etree.ElementTree as ET
from datetime import datetime
from pathlib import Path

from common import (
    ExitCode,
    exec_command,
    exit_message,
    get_env_variable,
    get_env_variable_required,
    get_last_version,
    get_version,
    is_path_exist,
    is_semver,
    unshallow_repo,
)
from common.defectdojo_helper import DefectDojoHelper
from common.sonar_helper import SonarHelper, SonarScannerType
from src.interface.project import ProjectInterface


class MavenProjectInterface(ProjectInterface):
    def __init__(self) -> None:
        super().__init__()

        self.java_impl_pom = "java/impl/pom.xml"
        self.pom_root = "pom.xml"

        if self.buildable_project.skip_build():
            return

        self.pom_xml = Path(self.pom_root)

        if not self.pom_xml.is_file():
            exit_message(
                "O projeto está usando o script de Maven porém não possui o pom.xml"
            )

        self.version = self._get_version()

    def set_version(self):
        self._update_pom_xmls()

        if self._using_tycho():
            self._execute_mvn_command(
                f"org.eclipse.tycho:tycho-versions-plugin:1.7.0:set-version -DnewVersion={self.version}"
            )
        else:
            self._execute_mvn_command(
                f"versions:set -DgenerateBackupPoms=false -DnewVersion={self.version} "
                "-DprocessAllModules -N versions:update-child-modules"
            )

    def validate(self):
        super().validate()

        if self.buildable_project.skip_build():
            return

        self._execute_mvn_command("validate")

    def generate(self):
        if self.buildable_project.skip_build():
            return

        self.set_version()

    def compile(self):
        if self.buildable_project.skip_build():
            return

        self._execute_mvn_command("compile")

    def unit_test(self):
        if self.buildable_project.skip_build():
            return

        self._docker_login()

        self._execute_mvn_command("test")

    def sonar_scanner(self):
        sonar_helper = SonarHelper(
            sonar_scanner_type=SonarScannerType.MAVEN,
            skip_build=self.buildable_project.skip_build(),
        )
        sonar_helper.scanner_analyze()

    def package(self):
        self._execute_mvn_command("deploy -DskipTests=true")

    def _execute_mvn_command(self, command):
        maven_args = get_env_variable("MAVEN_ARGS")

        if self.sci_debug:
            maven_args = f"{maven_args} --show-version -X"

        command_result_mvn = exec_command(f"mvn {maven_args} {command}")

        if command_result_mvn.exit_code == ExitCode.ERROR:
            exit_message("Maven command failed. Exiting...")

    def _get_version(self) -> str:
        """
        Retrieves the current version of the project, appending the '-SNAPSHOT' suffix
        if necessary to indicate a development version.
        https://maven.apache.org/guides/getting-started/#What_is_a_SNAPSHOT_version.3F
        """
        version = get_version()

        if self._using_tycho() and not is_semver(version):
            return f"{get_last_version()}-SNAPSHOT"

        return version if is_semver(version) else f"{version}-SNAPSHOT"

    def _using_tycho(self):
        return "<tycho-version>" in self.pom_xml.read_text(encoding="utf-8")

    def _update_pom_xmls(self):
        pom_xml_files = list(Path(".").rglob("pom.xml"))
        for pom_xml in pom_xml_files:
            content = pom_xml.read_text(encoding="utf-8")
            updated_content = re.sub(
                r"maven\.senior\.com\.br/artifactory",
                "nexus.senior.com.br/repository",
                content,
            )
            pom_xml.write_text(updated_content, encoding="utf-8")

    def build_pitest(self, pitest_command):
        """Executa o comando Maven com os argumentos do PIT."""
        original_dir = os.getcwd()

        if is_path_exist(self.java_impl_pom):
            pom_path = self.java_impl_pom
        else:
            pom_path = self.pom_root

        if not is_path_exist(pom_path):
            exit_message(f"Arquivo {pom_path} não encontrado.")

        pom_content = Path(pom_path).read_text(encoding="utf-8")
        if "pitest-maven" not in pom_content:
            exit_message("Plugin PITest ausente no pom.xml.")

        if "<scm>" not in pom_content:
            exit_message("Configuração de conexão do scm ausente no pom.xml.")

        cmd = f"mvn -f {pom_path} test-compile {pitest_command}"
        result = exec_command(
            cmd,
            print_output=True,
            error_message="Falha ao executar o comando PITest Maven",
        )

        os.chdir(original_dir)

        return result

    def _check_mutations_xml(self, xml_dir="target/pit-reports/"):

        file_path = f"{xml_dir}mutations.xml"
        if is_path_exist(file_path):
            return file_path
        else:
            return None

    def extract_text_from_element(self, element, tag):
        child = element.find(tag)
        return child.text if child is not None else ""

    def file_exists_in_repo(self, path):
        return os.path.isfile(path)

    def _convert_xml_to_json(self, xml_path):
        """Converte o mutations.xml do PIT para JSON compatível com DefectDojo."""
        tree = ET.parse(xml_path)
        root = tree.getroot()

        findings = []
        now = datetime.now().strftime("%Y-%m-%d")

        for mutation in root.findall(".//mutation"):
            status = mutation.get("status")
            detected = status == "KILLED"

            if detected:
                continue

            mutated_class = self.extract_text_from_element(mutation, "mutatedClass")
            mutated_method = self.extract_text_from_element(mutation, "mutatedMethod")
            method_description = self.extract_text_from_element(
                mutation, "methodDescription"
            )
            line_text = self.extract_text_from_element(mutation, "lineNumber")
            try:
                line_number = int(line_text) if line_text.isdigit() else 0
            except ValueError:
                line_number = 0

            mutator = self.extract_text_from_element(mutation, "mutator")
            mutator_short_name = mutator.split(".")[-1]

            description_text = self.extract_text_from_element(mutation, "description")
            mutated_class = self.extract_text_from_element(mutation, "mutatedClass")
            file_name = self.extract_text_from_element(mutation, "sourceFile")

            class_path_parts = mutated_class.split(".")
            package_path = "/".join(class_path_parts[:-1])

            relative_path = f"{package_path}/{file_name}"

            common_prefixes = [
                "src/main/java",
                "src/test/java",
                "app",
                "lib",
                "",
            ]

            for prefix in common_prefixes:
                file_path_candidate = f"{prefix}/{relative_path}".lstrip("/")
                if self.file_exists_in_repo(file_path_candidate):
                    file_path = file_path_candidate
                    break
            else:
                file_path = relative_path

            unique_id = f"{mutated_class}:{mutated_method}:{line_number}:{mutator}"

            mitigation_message = (
                f"Revise o método {mutated_method} da classe {mutated_class}. "
                f"Adicione ou ajuste os testes unitários para cobrir esse caminho de código e detectar alterações "
                f"causadas pelo mutator {mutator}."
            )

            finding = {
                "title": f"Mutation in {mutated_class}.{mutated_method} - Line {line_number} - {mutator_short_name}",
                "date": now,
                "severity": "Info",
                "description": (
                    f"Status: {status}\n"
                    f"Mutation: {description_text}\nMutator: {mutator}\n"
                    f"Method: {method_description}\nLine: {line_number}\n\n"
                    f"Consulte a [documentação oficial do PIT](https://pitest.org/) para mais detalhes.\n"
                ),
                "mitigation": mitigation_message,
                "file_path": file_path,
                "line": line_number,
                "static_finding": True,
                "active": True,
                "verified": True,
                "unique_id_from_tool": unique_id,
            }

            findings.append(finding)

        return json.dumps({"findings": findings}, indent=2)

    def mutation_test(self):
        """Executa testes de mutação para análise entre branches no CI."""
        target_branch = get_env_variable(
            "CI_MERGE_REQUEST_TARGET_BRANCH_NAME",
            get_env_variable_required("CI_DEFAULT_BRANCH"),
        )

        if target_branch and not target_branch.startswith("origin/"):
            target_branch = f"origin/{target_branch}"

        source_branch = get_env_variable(
            "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", get_env_variable("CI_COMMIT_BRANCH")
        )

        if source_branch and not source_branch.startswith("origin/"):
            source_branch = f"origin/{source_branch}"

        default_arguments = (
            "-DoutputFormats=XML,HTML -DwithHistory -DtimestampedReports=false "
            "-DtimeoutFactor=1.75 -Dthreads=2 -DtimeoutConstant=10000 -Djacoco.skip=true"
        )

        if target_branch == source_branch:
            pitest_command = (
                f"org.pitest:pitest-maven:mutationCoverage {default_arguments}"
            )
        else:
            scm_root_dir = "../../" if is_path_exist(self.java_impl_pom) else "."
            pitest_command = (
                f"org.pitest:pitest-maven:scmMutationCoverage -DoriginBranch={source_branch} "
                f"-Dinclude=ADDED,MODIFIED -DdestinationBranch={target_branch} "
                f"-DanalyseLastCommit=false -DscmRootDir={scm_root_dir} {default_arguments} "
            )

        unshallow_repo()
        return self.build_pitest(pitest_command)

    def mutation_report(self):
        """
        Método para importar report PIT no Defect Dojo.
        Caso não encontre o report retorna True.
        Caso contrário, retorna o engagement criado.
        """
        xml_path = self._check_mutations_xml()

        if xml_path is None:
            return None

        json_data = self._convert_xml_to_json(xml_path)

        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".json", delete=False
        ) as tmp_file:
            tmp_file.write(json_data)
            json_file_path = tmp_file.name

        defectdojo_helper = DefectDojoHelper()
        engagement = super().import_scans(defectdojo_helper)

        data = {
            "test_type": 76,
            "test_title": "PIT Mutation Testing",
            "scan_type": "Generic Findings Import",
        }

        files = {
            "file": (
                f"mutations_{defectdojo_helper.ci_project_name}.json",
                open(json_file_path, "rb"),
                "application/json",
            )
        }

        defectdojo_helper.import_scan(engagement["id"], data, files)

        super().print_engagement_url(defectdojo_helper, engagement)

        return engagement

    def defect_dojo_import(self):
        """
        Importa os resultados dos testes de mutação para o DefectDojo.
        Este método é chamado pelo script defect_dojo_import.py.
        """
        engagement = self.mutation_report()
        if not engagement:
            super().defect_dojo_import()
