# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import re

from odoo import _, api, fields, models


class CxTowerGitProject(models.Model):
    """
    Git Project.
    Implements pre-defined git configuration.
    """

    _name = "cx.tower.git.project"
    _description = "Cetmix Tower Git Project"
    _order = "name"

    _inherit = [
        "cx.tower.reference.mixin",
        "cx.tower.yaml.mixin",
        "cx.tower.access.role.mixin",
    ]

    def _get_post_create_fields(self):
        res = super()._get_post_create_fields()
        return res + [
            "source_ids",
            "git_project_rel_ids",
            "git_project_file_template_rel_ids",
        ]

    active = fields.Boolean(default=True)
    # IMPORTANT: This field may contain duplicates because of the relation nature!
    server_ids = fields.Many2many(
        comodel_name="cx.tower.server",
        relation="cx_tower_git_project_server_rel",
        readonly=True,
        copy=False,
        compute="_compute_server_ids",
        store=True,
        context={"active_test": False},
        help="Servers are added automatically based on the files"
        " linked to the project.",
    )
    source_ids = fields.One2many(
        comodel_name="cx.tower.git.source",
        inverse_name="git_project_id",
        string="Sources",
        auto_join=True,
        copy=True,
    )
    git_project_rel_ids = fields.One2many(
        comodel_name="cx.tower.git.project.rel",
        inverse_name="git_project_id",
        string="Git Project Server File Relations",
        copy=False,
    )
    # Helper field to get all files related to git project
    file_ids = fields.Many2many(
        comodel_name="cx.tower.file",
        relation="cx_tower_git_project_rel",
        column1="git_project_id",
        column2="file_id",
        string="Files",
        readonly=True,
        depends=["git_project_rel_ids"],
        copy=False,
    )
    git_project_file_template_rel_ids = fields.One2many(
        comodel_name="cx.tower.git.project.file.template.rel",
        inverse_name="git_project_id",
        string="Git Project File Template Relations",
        copy=False,
    )
    # Helper field to get all file templates related to git project
    file_template_ids = fields.Many2many(
        comodel_name="cx.tower.file.template",
        relation="cx_tower_git_project_file_template_rel",
        column1="git_project_id",
        column2="file_template_id",
        string="File Templates",
        readonly=True,
        depends=["git_project_file_template_rel_ids"],
        copy=False,
    )
    # Helper field to get all repositories used in this project
    repo_ids = fields.Many2many(
        comodel_name="cx.tower.git.repo",
        relation="cx_tower_git_repo_project_rel",
        column1="project_id",
        column2="repo_id",
        string="Repositories",
        readonly=True,
        copy=False,
        help="Repositories used in this project through its sources and remotes",
    )
    note = fields.Text()

    # ---- Access. Add relation for mixin fields
    user_ids = fields.Many2many(
        relation="cx_tower_git_project_user_rel",
        compute="_compute_user_ids",
        readonly=False,
        store=True,
        precompute=True,
        domain=lambda self: [
            ("groups_id", "in", self.env.ref("cetmix_tower_server.group_manager").ids)
        ],
    )
    manager_ids = fields.Many2many(
        relation="cx_tower_git_project_manager_rel",
        compute="_compute_user_ids",
        readonly=False,
        store=True,
        precompute=True,
    )

    # -- UI/UX fields
    has_private_remotes = fields.Boolean(
        compute="_compute_has_private_remotes",
        help="Indicates if the project has any private remotes.",
    )
    has_partially_private_remotes = fields.Boolean(
        compute="_compute_has_private_remotes",
        help="Indicates if the project has any partially private remotes.",
    )

    # -- Git Aggregator related fields
    git_aggregator_root_dir = fields.Char(
        help="Git aggregator root directory where sources will be cloned."
        " Eg '/tmp/git-aggregator'"
        " Will use '.' if not set",
    )

    def _selection_project_format(self):
        """
        Possible project formats.
        Inherit and extend when adding new project formats.

        Returns:
            List of tuples: (code, name)
        """
        return [
            ("git_aggregator", "Git Aggregator"),
        ]

    def _default_project_format(self):
        """
        Default project format.
        """
        return "git_aggregator"

    @api.depends("git_project_rel_ids", "git_project_rel_ids.server_id")
    def _compute_server_ids(self):
        """Compute server ids for git projects.

        Why? Because a git project can be linked to multiple files
        on the same server.
        So we need to use a set to avoid duplicates so every server
        is listed only once.
        """
        for project in self:
            project.server_ids = (
                list(set(project.git_project_rel_ids.server_id.ids))
                if project.git_project_rel_ids
                else False
            )

    @api.depends(
        "git_project_rel_ids.server_id",
        "git_project_rel_ids.server_id.user_ids",
        "git_project_rel_ids.server_id.manager_ids",
    )
    def _compute_user_ids(self):
        """
        Users. All users who have "Manager" group and are either set in "Users"
        or in "Managers" in all related servers.
        Managers. All users who have "Manager" group and are set as "Managers"
        in all related servers.

        This is done to avoid unpredictable consequences when some of the servers
        are not updated due to access restrictions when a project is updated.
        """
        for project in self:
            # Do not compute if no servers are related
            server_ids = project.git_project_rel_ids.server_id
            if not server_ids:
                continue

            # Get all user and manager ids from related servers
            all_user_ids = server_ids.user_ids.filtered(
                lambda u: u.has_group("cetmix_tower_server.group_manager")
            ).ids
            all_manager_ids = server_ids.manager_ids.ids

            # Create a final list of user and manager ids
            user_ids = []
            manager_ids = []
            # Check if user is present in all servers
            for user_id in all_user_ids:
                if all(
                    user_id in server.user_ids.ids or user_id in server.manager_ids.ids
                    for server in server_ids
                ):
                    user_ids.append(user_id)
            # Check if manager is present in all servers
            for manager_id in all_manager_ids:
                if all(manager_id in server.manager_ids.ids for server in server_ids):
                    manager_ids.append(manager_id)

            # Set the final lists
            project.update(
                {
                    "user_ids": [(6, 0, user_ids)],
                    "manager_ids": [(6, 0, manager_ids)],
                }
            )

    @api.depends(
        "source_ids", "source_ids.remote_ids", "source_ids.remote_ids.is_private"
    )
    def _compute_has_private_remotes(self):
        for project in self:
            project.has_private_remotes = any(
                source.remote_count > 0
                and source.remote_count_private == source.remote_count
                for source in project.source_ids
            )
            project.has_partially_private_remotes = any(
                source.remote_count_private > 0
                and source.remote_count_private != source.remote_count
                for source in project.source_ids
            )

    @api.model_create_multi
    def create(self, vals_list):
        res = super().create(vals_list)
        # Update related files and templates on create
        res._update_related_files_and_templates()
        return res

    def write(self, vals):
        res = super().write(vals)
        # Update related files and templates on update
        self._update_related_files_and_templates()
        return res

    # ------------------------------
    # Helper methods
    # ------------------------------
    def _update_related_files_and_templates(self):
        # Update related files and templates
        if self.git_project_rel_ids:
            self.git_project_rel_ids._save_to_file()
        if self.git_project_file_template_rel_ids:
            self.git_project_file_template_rel_ids._save_to_file_template()

    def _extract_variables_from_text(self, text):
        """Extract environment variables from text.
        Helper method for file content generation.

        Args:
            text (str): Text to extract variables from
        Returns:
            List: List of variables
        """
        variables = re.findall(r"\$([A-Z0-9_]+)", text)
        return sorted(list(set(variables)))

    def _compose_copy_name(self, server=False):
        """
        Compose copy name of a git project copy.
        Helper method used when creating a copy of a git project.

        Args:
            server (cx.tower.server): Server to get the copy name for.

        Returns:
            Char: Copy name
        """
        self.ensure_one()
        if server:
            return server.name
        return _("%(name)s (copy)", name=self.name)

    # ------------------------------
    # YAML mixin methods
    # ------------------------------
    def _get_fields_for_yaml(self):
        res = super()._get_fields_for_yaml()
        res += [
            "name",
            "note",
            "source_ids",
            "git_aggregator_root_dir",
        ]
        return res

    # -------------------------------
    # Git Aggregator related methods
    # -------------------------------
    def _git_aggregator_prepare_record(self):
        """Prepare json structure for git aggregator.

        Returns:
            Dict: Json structure for git aggregator
        """
        self.ensure_one()
        values = {}
        for source in self.source_ids:
            if source.enabled and source.remote_count:
                root_dir = self.git_aggregator_root_dir or "."
                values.update(
                    {
                        f"/{source.reference}"
                        if root_dir == "/"
                        else f"{root_dir}/{source.reference}": source._git_aggregator_prepare_record()  # noqa: E501
                    }
                )
        return values

    def _git_aggregator_prepare_yaml_comment(self, yaml_code):
        """Generate commentary for yaml file.
        It includes brief instructions for git aggregator
        and lists environment variables that are required.

        Args:
            yaml_code (str): Yaml code

        Returns:
            Char: comment text or None
        """

        comment_text = _(
            "# This file is generated with Cetmix Tower https://cetmix.com/tower\n"
            "# It's designed to be used with git-aggregator tool developed by Acsone.\n"
            "# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n"
        )
        variable_list = self._extract_variables_from_text(yaml_code)
        if variable_list:
            comment_text += _(
                "\n# You need to set the following variables in your environment:\n# %(vars)s\n"  # noqa: E501
                "# and run git-aggregator with '--expand-env' parameter.\n",  # noqa: E501
                vars=(", ".join(variable_list)),
            )
        return comment_text

    def _generate_code_git_aggregator(self, record):
        """Generate code in git-aggregator format.

        Args:
            record (recordset()): Model record to generate code for.
                must be a single record and have git_project_id field.

        Returns:
            Text: Yaml code
        """
        yaml_mixin = self.env["cx.tower.yaml.mixin"]

        # Do not generate code if record values are empty
        record_values = record.git_project_id._git_aggregator_prepare_record()
        if record_values:
            yaml_code = yaml_mixin._convert_dict_to_yaml(record_values)
            # Prepend comment to yaml code
            comment = record.git_project_id._git_aggregator_prepare_yaml_comment(
                yaml_code
            )
            return f"{comment}\n{yaml_code}"
        return ""
