import math
import os
import re
import tempfile
from pathlib import Path

import lizard

from metripy.Application.Config.ProjectConfig import ProjectConfig
from metripy.LangAnalyzer.AbstractLangAnalyzer import AbstractLangAnalyzer
from metripy.LangAnalyzer.Php.Ast.PhpAstParser import PhpAstParser
from metripy.LangAnalyzer.Php.Metrics.PhpCognitiveComplexityCalculator import (
    PhpCognitiveComplexityCalculator,
)
from metripy.LangAnalyzer.Php.PhpBasicAstParser import PhpBasicAstParser
from metripy.LangAnalyzer.Php.PhpImportsAnalyzer import PhpImportsAnalyzer
from metripy.Tree.ClassNode import ClassNode
from metripy.Tree.FunctionNode import FunctionNode
from metripy.Tree.ModuleNode import ModuleNode


class PhpAnalyzer(AbstractLangAnalyzer):
    def __init__(self, project_config: ProjectConfig):
        super().__init__(project_config)

    def get_lang_name(self) -> str:
        return "PHP"

    def get_supported_extensions(self) -> tuple[str]:
        return (".php",)

    def _create_lizard_analyzable_file(self, filename: str) -> str:
        """
        Because of a bug in lizard it cannot correctly analyze traits.
        See https://github.com/terryyin/lizard/issues/441
        Because of a bug in lizard we need to replace use function statements.
        See https://github.com/terryyin/lizard/issues/442
        """
        tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".php")
        with open(filename, "r") as f:
            code = f.readlines()
            code = [re.sub(r"^(\s*)trait\s+", r"\1class ", line) for line in code]
            # use remove the function keyword, so it gets counted the same way and dont increate blank lines / lower loc
            code = [
                re.sub(r"^(\s*)use\s+function\s+", r"\1use ", line) for line in code
            ]
            tmp_file.writelines(code)
            tmp_file.close()
        return tmp_file.name

    def analyze(self, code: str, filename: str) -> None:
        file_stem = Path(filename).stem
        structure = PhpBasicAstParser.parse_php_structure(code)

        is_tmp_file = False
        filename_to_analyze = filename
        if "trait" in filename.lower() or "use function" in code:
            filename_to_analyze = self._create_lizard_analyzable_file(filename)
            is_tmp_file = True

        lizard_result = lizard.analyze_file(filename_to_analyze)
        complexity_data = {
            func.name: {
                "complexity": func.cyclomatic_complexity,
                "start_line": func.start_line,
                "end_line": func.end_line,
            }
            for func in lizard_result.function_list
        }

        if is_tmp_file:
            os.unlink(filename_to_analyze)

        classes: dict[str, ClassNode] = {}
        functions: dict[str, FunctionNode] = {}
        for obj in structure:
            if obj["type"] == "class":
                full_name = self.full_name(filename, obj["name"])
                classes[full_name] = ClassNode(
                    full_name,
                    obj["name"],
                    obj["line"],
                    0,
                    0,  # gets filled in later, based on methods
                )
            elif obj["type"] == "method" or obj["type"] == "function":
                full_name = self.full_name(filename, obj["name"])
                try:
                    function_node = FunctionNode(
                        full_name,
                        obj["name"],
                        obj["line"],
                        0,
                        complexity_data[f"{file_stem}::{obj['name']}"]["complexity"],
                    )
                    function_node.line_end = complexity_data[
                        f"{file_stem}::{obj['name']}"
                    ]["end_line"]
                except KeyError:
                    # no complexity data, function must be empty
                    function_node = FunctionNode(
                        full_name,
                        obj["name"],
                        obj["line"],
                        0,
                        0,
                    )

                if obj["type"] == "method":
                    class_name = obj["class"]
                    full_class_name = self.full_name(filename, class_name)
                    class_node = classes.get(full_class_name)
                    if class_node is not None:
                        class_node.functions.append(function_node)
                    else:
                        raise ValueError(
                            f"Class node not found for function {full_class_name}"
                        )
                functions[full_name] = function_node

        # complexity of classes
        for class_node in classes.values():
            class_node.real_complexity = sum(
                [func.complexity for func in class_node.functions]
            )

        module_node = self.create_module_node(filename, code)
        module_node.classes.extend(classes.values())
        module_node.functions.extend(functions.values())

        code_lines = code.split("\n")
        for func_name, function_node in functions.items():
            lines = code_lines[function_node.lineno : function_node.line_end]
            self.add_function_halstead_metrics(function_node, "\n".join(lines))

        maintainability_index = self._calculate_maintainability_index(
            functions.values(), module_node
        )
        module_node.maintainability_index = maintainability_index
        self.modules[module_node.full_name] = module_node

        # ignore for now
        # self.duplicate_detector.add_code(filename, code)

        imports_analyzer = PhpImportsAnalyzer(filename, code)
        module_node.import_name = imports_analyzer.extract_import_name()
        module_node.imports = imports_analyzer.extract_imports()

        module_node.code_smells = self.code_smell_detector.detect_all(filename, code)

        parser = PhpAstParser()
        parser.parse(code)
        cognitive_complexities = PhpCognitiveComplexityCalculator(
            parser
        ).calculate_for_all_functions()
        for func_name, complexity in cognitive_complexities.items():
            full_name = self.full_name(filename, func_name)
            function_node = functions.get(full_name)
            if function_node is not None:
                function_node.cognitive_complexity = complexity
            else:
                raise ValueError(f"Function node not found for function {full_name}")

    def _calculate_maintainability_index(
        self, functions: list[FunctionNode], module_node: ModuleNode
    ) -> float:
        """Calculate maintainability index for PHP"""
        if not functions:
            return 100.0

        total_volume = sum(func.volume for func in functions)
        total_complexity = sum(func.complexity for func in functions)
        total_length = sum(func.length for func in functions)

        if total_volume == 0 or total_length == 0:
            return 100.0

        # PHP maintainability index calculation
        mi_base = max(
            (
                171
                - 5.2 * math.log(total_volume)
                - 0.23 * total_complexity
                - 16.2 * math.log(total_length)
            )
            * 100
            / 171,
            0,
        )

        # Comment weight
        comment_weight = 0
        if module_node.loc > 0:
            comment_ratio = module_node.single_comments / module_node.loc
            comment_weight = 50 * math.sin(math.sqrt(2.4 * comment_ratio))

        return mi_base + comment_weight
