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

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class CxTowerServerTemplate(models.Model):
    """Server Template. Used to simplify server creation"""

    _name = "cx.tower.server.template"
    _inherit = [
        "cx.tower.reference.mixin",
        "mail.thread",
        "mail.activity.mixin",
        "cx.tower.access.role.mixin",
        "cx.tower.tag.mixin",
    ]
    _description = "Cetmix Tower Server Template"
    _order = "name"

    active = fields.Boolean(default=True)

    # --- Connection
    ssh_port = fields.Integer(string="SSH port", default=22)
    ssh_username = fields.Char(string="SSH Username")
    ssh_password = fields.Char(string="SSH Password")
    ssh_key_id = fields.Many2one(
        comodel_name="cx.tower.key",
        string="SSH Private Key",
        domain=[("key_type", "=", "k")],
    )
    ssh_auth_mode = fields.Selection(
        string="SSH Auth Mode",
        selection=[
            ("p", "Password"),
            ("k", "Key"),
        ],
    )
    use_sudo = fields.Selection(
        string="Use sudo",
        selection=[("n", "Without password"), ("p", "With password")],
        help="Run commands using 'sudo'",
    )

    # --- Attributes
    color = fields.Integer(help="For better visualization in views")
    os_id = fields.Many2one(string="Operating System", comodel_name="cx.tower.os")
    tag_ids = fields.Many2many(
        relation="cx_tower_server_template_tag_rel",
        column1="server_template_id",
        column2="tag_id",
    )

    # --- Variables
    # We are not using variable mixin because we don't need to parse values
    variable_value_ids = fields.One2many(
        string="Variable Values",
        comodel_name="cx.tower.variable.value",
        auto_join=True,
        inverse_name="server_template_id",
    )

    # --- Server logs
    server_log_ids = fields.One2many(
        comodel_name="cx.tower.server.log", inverse_name="server_template_id"
    )

    # --- Shortcuts
    shortcut_ids = fields.Many2many(
        comodel_name="cx.tower.shortcut",
        relation="cx_tower_server_template_shortcut_rel",
        column1="server_template_id",
        column2="shortcut_id",
        string="Shortcuts",
    )

    # --- Scheduled Tasks
    scheduled_task_ids = fields.Many2many(
        comodel_name="cx.tower.scheduled.task",
        relation="cx_tower_server_template_scheduled_task_rel",
        column1="server_template_id",
        column2="scheduled_task_id",
        string="Scheduled Tasks",
    )

    # --- Flight Plan
    flight_plan_id = fields.Many2one(
        "cx.tower.plan",
        help="This flight plan will be run upon server creation",
        domain="[('server_ids', '=', False)]",
    )

    # ---- Delete plan
    plan_delete_id = fields.Many2one(
        "cx.tower.plan",
        string="On Delete Plan",
        groups="cetmix_tower_server.group_manager",
        help="This Flightplan will be executed when the server is deleted",
    )

    # --- Created Servers
    server_ids = fields.One2many(
        comodel_name="cx.tower.server",
        inverse_name="server_template_id",
    )
    server_count = fields.Integer(
        compute="_compute_server_count",
    )

    # -- Other
    note = fields.Text()

    # ---- Access. Add relation for mixin fields
    user_ids = fields.Many2many(
        relation="cx_tower_server_template_user_rel",
        domain=lambda self: [
            ("groups_id", "in", [self.env.ref("cetmix_tower_server.group_manager").id])
        ],
    )
    manager_ids = fields.Many2many(
        relation="cx_tower_server_template_manager_rel",
    )

    @api.depends("server_ids")
    def _compute_server_count(self):
        """
        Compute total server counts created from the templates
        """
        for template in self:
            template.server_count = len(template.server_ids)

    def copy(self, default=None):
        """Duplicate the server template along with variable values and server logs."""
        default = dict(default or {})

        # Duplicate the server template itself
        new_template = super().copy(default)

        # Duplicate variable values
        for variable_value in self.variable_value_ids:
            variable_value.with_context(reference_mixin_skip_self=True).copy(
                {"server_template_id": new_template.id}
            )

        # Duplicate server logs
        for server_log in self.server_log_ids:
            server_log.copy({"server_template_id": new_template.id})

        return new_template

    def action_create_server(self):
        """
        Returns wizard action to create new server
        """
        self.ensure_one()
        context = self.env.context.copy()
        context.update(
            {
                "default_server_template_id": self.id,  # pylint: disable=no-member
                "default_color": self.color,
                "default_ssh_port": self.ssh_port,
                "default_ssh_username": self.ssh_username,
                "default_ssh_password": self.ssh_password,
                "default_ssh_key_id": self.ssh_key_id.id,
                "default_ssh_auth_mode": self.ssh_auth_mode,
                "default_plan_delete_id": self.plan_delete_id.id,
            }
        )
        if self.variable_value_ids:
            context.update(
                {
                    "default_line_ids": [
                        (
                            0,
                            0,
                            {
                                "variable_value_id": line.id,
                            },
                        )
                        for line in self.variable_value_ids
                    ]
                }
            )
        return {
            "type": "ir.actions.act_window",
            "name": _("Create Server"),
            "res_model": "cx.tower.server.template.create.wizard",
            "view_mode": "form",
            "target": "new",
            "context": context,
        }

    def action_open_servers(self):
        """
        Return action to open related servers
        """
        self.ensure_one()
        action = self.env["ir.actions.act_window"]._for_xml_id(
            "cetmix_tower_server.action_cx_tower_server"
        )
        action.update(
            {
                "domain": [("server_template_id", "=", self.id)],  # pylint: disable=no-member
            }
        )
        return action

    @api.model
    def create_server_from_template(self, template_reference, server_name, **kwargs):
        """This is a wrapper function that is meant to be called
        when we need to create a server from specific server template

        Args:
            template_reference (Char): Server template reference
            server_name (Char): Name of the new server

        Kwargs:
            partner (res.partner(), optional): Partner this server belongs to.
            ipv4 (Char, optional): IP v4 address. Defaults to None.
            ipv6 (Char, optional): IP v6 address.
                Must be provided in case IP v4 is not. Defaults to None.
            ssh_password (Char, optional): SSH password. Defaults to None.
            ssh_key (Char, optional): SSH private key record reference.
                Defaults to None.
            configuration_variables (Dict, optional): Custom configuration variable.
                Following format is used:
                    `variable_reference`: `variable_value_char`
                    eg:
                    {'branch': 'prod', 'odoo_version': '16.0'}
            pick_all_template_variables (bool):  This parameter ensures that the server
                being created considers existing variables from the template.
                If enabled, the template variables will also be included in the server
                variables. The default value is True.

        Returns:
            cx.tower.server: newly created server record
        """
        template = self.get_by_reference(template_reference)
        return template._create_new_server(server_name, **kwargs)

    def _create_new_server(self, name, **kwargs):
        """Creates a new server from template

        Args:
            name (Char): Name of the new server

        Kwargs:
            partner (res.partner(), optional): Partner this server belongs to.
            ipv4 (Char, optional): IP v4 address. Defaults to None.
            ipv6 (Char, optional): IP v6 address.
                Must be provided in case IP v4 is not. Defaults to None.
            ssh_password (Char, optional): SSH password. Defaults to None.
            ssh_key (Char, optional): SSH private key record reference.
                Defaults to None.
            configuration_variables (Dict, optional): Custom configuration variable.
                Following format is used:
                    `variable_reference`: `variable_value_char`
                    eg:
                    {'branch': 'prod', 'odoo_version': '16.0'}
            pick_all_template_variables (bool):  This parameter ensures that the server
                being created considers existing variables from the template.
                If enabled, the template variables will also be included in the server
                variables. The default value is True.

        Returns:
            cx.tower.server: newly created server record
        """
        self.ensure_one()

        # Retrieve the passed variables
        configuration_variables = kwargs.get("configuration_variables", {})

        # We validate mandatory variables
        if not kwargs.get("pick_all_template_variables"):
            self._validate_required_variables(configuration_variables)

        # We are using sudo to ensure all values are copied
        server_values = self.sudo()._prepare_server_values(
            name=name,
            server_template_id=self.id,  # pylint: disable=no-member
            **kwargs,
        )

        # Pop variable values to add them after server creation.
        # This is needed to ensure that access rules are applied properly.
        variable_values = server_values.pop("variable_value_ids")

        # Prepare context for server creation
        context = self.env.context.copy()

        # SSH setting may be added after server creation.
        context.update({"skip_ssh_settings_check": True})
        # We need to remove default_server_template_id to avoid it being used
        # in variable values.
        context.pop("default_server_template_id", None)

        # Create server
        server = (
            self.env["cx.tower.server"]  # pylint: disable=context-overridden # new need a new clean context
            .sudo()
            .with_context(context)
            .create(server_values)
            .sudo()
        )

        # Add variable values
        if variable_values:
            server.with_context(context).write({"variable_value_ids": variable_values})  # pylint: disable=context-overridden # new need a new clean context

        # Create server logs
        logs = server.server_log_ids.filtered(lambda rec: rec.log_type == "file")
        for log in logs.sudo():
            log.file_id = log.file_template_id.create_file(
                server=server, if_file_exists="skip"
            ).id

        flight_plan = server.server_template_id.flight_plan_id
        if flight_plan:
            server.run_flight_plan(flight_plan)

        return server

    def _get_post_create_fields(self):
        """
        Add fields that should be populated after server template creation
        """
        res = super()._get_post_create_fields()
        return res + ["variable_value_ids", "server_log_ids"]

    def _get_fields_tower_server(self):
        """
        Return field name list to read from template and create new server
        """
        return [
            "ssh_username",
            "ssh_password",
            "ssh_key_id",
            "ssh_auth_mode",
            "use_sudo",
            "color",
            "os_id",
            "plan_delete_id",
            "tag_ids",
            "variable_value_ids",
            "server_log_ids",
            "shortcut_ids",
            "scheduled_task_ids",
        ]

    def _prepare_server_values(self, pick_all_template_variables=True, **kwargs):
        """
        Prepare the server values to create a new server based on
        the current template. It reads all fields from the template, copies them,
        and processes One2many fields to create new related records. Magic fields
        like 'id', concurrency fields, and audit fields are excluded from the copied
        data.

        Args:
            pick_all_template_variables (bool):  This parameter ensures that the server
                being created considers existing variables from the template.
                If enabled, the template variables will also be included in the server
                variables. The default value is True.
            **kwargs: Additional values to update in the final server record.

        Returns:
            list: A list of dictionaries representing the values for the new server
                  records.
        """
        model_fields = self._fields
        field_o2m_type = fields.One2many

        # define the magic fields that should not be copied
        # (including ID and concurrency fields)
        MAGIC_FIELDS = models.MAGIC_COLUMNS + [self.CONCURRENCY_CHECK_FIELD]

        # read all values required to create a new server from the template
        values = self.read(self._get_fields_tower_server(), load=False)[0]

        # prepare server config values from kwargs
        server_config_values = self._parse_server_config_values(kwargs)
        template = self.browse(values["id"])

        # Process each field in the template
        for field in values.keys():
            if isinstance(model_fields[field], field_o2m_type):
                # get related records for One2many field
                related_records = getattr(template, field)
                new_records = []
                # for each related record, read its data and prepare it for copying
                for record in related_records:
                    record_data = {
                        k: v
                        for k, v in record.read(load=False)[0].items()
                        if k not in MAGIC_FIELDS
                    }
                    # set the inverse field (link back to the template)
                    # to False to unlink from the original template
                    record_data[model_fields[field].inverse_name] = False
                    new_records.append((0, 0, record_data))

                values[field] = new_records

        # Handle configuration variables if provided.
        configuration_variables = kwargs.pop("configuration_variables", None)
        configuration_variable_options = kwargs.pop(
            "configuration_variable_options", {}
        )

        if configuration_variables:
            # Validate required variables
            self._validate_required_variables(configuration_variables)

            # Search for existing variable options.
            option_references = list(configuration_variable_options.values())
            existing_options = option_references and self.env[
                "cx.tower.variable.option"
            ].search([("reference", "in", option_references)])
            missing_options = list(
                set(option_references)
                - {option.reference for option in existing_options}
            )

            if missing_options:
                # Map variable references to their corresponding
                # invalid option references.
                missing_options_to_variables = {
                    var_ref: opt_ref
                    for var_ref, opt_ref in configuration_variable_options.items()
                    if opt_ref in missing_options
                }
                # Generate a detailed error message for invalid variable options.
                detailed_message = "\n".join(
                    _(
                        "Variable reference '%(var_ref)s' has an invalid "
                        "option reference '%(opt_ref)s'.",
                        var_ref=var_ref,
                        opt_ref=opt_ref,
                    )
                    for var_ref, opt_ref in missing_options_to_variables.items()
                )
                raise ValidationError(
                    _(
                        "Some variable options are invalid:\n%(detailed_message)s",
                        detailed_message=detailed_message,
                    )
                )

            # Map variable options to their IDs.
            configuration_variable_options_dict = {
                option.variable_id.id: option for option in existing_options
            }

            variable_obj = self.env["cx.tower.variable"]
            variable_references = list(configuration_variables.keys())

            # Search for existing variables or create new ones if missing.
            exist_variables = variable_obj.search(
                [("reference", "in", variable_references)]
            )
            missing_references = list(
                set(variable_references)
                - {variable.reference for variable in exist_variables}
            )
            variable_vals_list = [
                {"name": reference} for reference in missing_references
            ]
            new_variables = variable_obj.create(variable_vals_list)
            all_variables = exist_variables | new_variables

            # Build a dictionary {variable: variable_value}.
            configuration_variable_dict = {
                variable: configuration_variables[variable.reference]
                for variable in all_variables
            }

            server_variable_vals_list = []
            for variable, variable_value in configuration_variable_dict.items():
                variable_option = configuration_variable_options_dict.get(variable.id)

                server_variable_vals_list.append(
                    (
                        0,
                        0,
                        {
                            "variable_id": variable.id,
                            "value_char": variable_option
                            and variable_option.value_char
                            or variable_value,
                            "option_id": variable_option and variable_option.id,
                        },
                    )
                )

            if pick_all_template_variables:
                # update or add variable values
                existing_variable_values = values.get("variable_value_ids", [])
                variable_id_to_index = {
                    cmd[2]["variable_id"]: idx
                    for idx, cmd in enumerate(existing_variable_values)
                    if cmd[0] == 0 and "variable_id" in cmd[2]
                }

                # Update exist variable options
                for exist_variable_id, index in variable_id_to_index.items():
                    option = configuration_variable_options_dict.get(exist_variable_id)
                    if not option:
                        continue
                    existing_variable_values[index][2].update(
                        {
                            "option_id": option.id,
                            "value_char": option.value_char,
                        }
                    )

                # Prepare new command values for server variables
                for new_command in server_variable_vals_list:
                    variable_id = new_command[2]["variable_id"]
                    if variable_id in variable_id_to_index:
                        idx = variable_id_to_index[variable_id]
                        # update exist command
                        existing_variable_values[idx] = new_command
                    else:
                        # add new command
                        existing_variable_values.append(new_command)

                values["variable_value_ids"] = existing_variable_values
            else:
                values["variable_value_ids"] = server_variable_vals_list

        # remove the `id` field to ensure a new record is created
        # instead of updating the existing one
        del values["id"]
        # update the values with additional arguments from kwargs
        values.update(kwargs)
        # update server configs
        values.update(server_config_values)
        # Add current user as user/manager to the newly created server
        values.update(
            {
                "user_ids": [(6, 0, self._default_user_ids())],
                "manager_ids": [(6, 0, self._default_manager_ids())],
            }
        )

        return values

    def _parse_server_config_values(self, config_values):
        """
        Prepares server configuration values.

        Args:
            config_values (dict): A dictionary containing server configuration values.
                Keys and their expected values:
                    - partner (res.partner, optional): The partner this server
                      belongs to.
                    - ipv4 (str, optional): IPv4 address. Defaults to None.
                    - ipv6 (str, optional): IPv6 address. Must be provided if IPv4 is
                      not specified. Defaults to None.
                    - ssh_key (str, optional): Reference to an SSH private key record.
                      Defaults to None.

        Returns:
            dict: A dictionary containing parsed server configuration values with the
                following keys:
                    - partner_id (int, optional): ID of the partner.
                    - ssh_key_id (int, optional): ID of the associated SSH key.
                    - ip_v4_address (str, optional): Parsed IPv4 address.
                    - ip_v6_address (str, optional): Parsed IPv6 address.
        """
        values = {}

        # This field is always populated from Server Template and
        # cannot be altered with function params.
        config_values.pop("plan_delete_id", None)

        partner = config_values.pop("partner", None)
        if partner:
            values["partner_id"] = partner.id

        ssh_key_reference = config_values.pop("ssh_key", None)
        if ssh_key_reference:
            ssh_key = self.env["cx.tower.key"].get_by_reference(ssh_key_reference)
            if ssh_key:
                values["ssh_key_id"] = ssh_key.id

        ipv4 = config_values.pop("ipv4", None)
        if ipv4:
            values["ip_v4_address"] = ipv4

        ipv6 = config_values.pop("ipv6", None)
        if ipv6:
            values["ip_v6_address"] = ipv6

        return values

    def _validate_required_variables(self, configuration_variables):
        """
        Validate that all required variables are present, not empty,
        and that no required variable is entirely missing from the configuration.

        Args:
            configuration_variables (dict): A dictionary of variable references
                                             and their values.

        Raises:
            ValidationError: If all required variables are
                            missing from the configuration,
                            or if any required variable is empty or missing.
        """
        required_variables = self.variable_value_ids.filtered("required")
        if not required_variables:
            return

        required_refs = [var.variable_reference for var in required_variables]
        config_refs = list(configuration_variables.keys())

        missing_variables = [ref for ref in required_refs if ref not in config_refs]
        empty_variables = [
            ref
            for ref in required_refs
            if ref in config_refs and not configuration_variables[ref]
        ]

        if not (missing_variables or empty_variables):
            return

        error_parts = [
            _("Please resolve the following issues with configuration variables:")
        ]

        if missing_variables:
            error_parts.append(
                _(
                    "  - Missing variables: %(variables)s",
                    variables=", ".join(missing_variables),
                )
            )

        if empty_variables:
            error_parts.append(
                _(
                    "  - Empty values for variables: %(variables)s",
                    variables=", ".join(empty_variables),
                )
            )

        raise ValidationError("\n".join(error_parts))

    def _get_dependent_model_relation_fields(self):
        """Check cx.tower.reference.mixin for the function documentation"""
        res = super()._get_dependent_model_relation_fields()
        return res + ["variable_value_ids"]
