# Part of Odoo. See LICENSE file for full copyright and licensing details.

import os
from collections import defaultdict
from datetime import date, datetime

from odoo import fields, models
from odoo.tools import groupby


def get_twilio_credentials(env) -> (str, str):
    """
    To be overridable if we need to obtain credentials from another source.
    :return: tuple(account_sid: str, auth_token: str)
    """
    params = env["ir.config_parameter"].sudo()
    account_sid = params.get_param("mail.twilio_account_sid")
    auth_token = params.get_param("mail.twilio_account_token")
    return account_sid, auth_token


def get_sfu_url(env) -> str | None:
    sfu_url = env['ir.config_parameter'].sudo().get_param("mail.sfu_server_url")
    if not sfu_url:
        sfu_url = os.getenv("ODOO_SFU_URL")
    if sfu_url:
        return sfu_url.rstrip("/")


def get_sfu_key(env) -> str | None:
    sfu_key = env['ir.config_parameter'].sudo().get_param('mail.sfu_server_key')
    if not sfu_key:
        return os.getenv("ODOO_SFU_KEY")
    return sfu_key


ids_by_model = defaultdict(lambda: ("id",))
ids_by_model.update(
    {
        "mail.thread": ("model", "id"),
        "MessageReactions": ("message", "content"),
        "Rtc": (),
        "Store": (),
    }
)

ONE = {}
MANY = {}


class Store:
    """Helper to build a dict of data for sending to web client.
    It supports merging of data from multiple sources, either through list extend or dict update.
    The keys of data are the name of models as defined in mail JS code, and the values are any
    format supported by store.insert() method (single dict or list of dict for each model name)."""

    def __init__(self, data=None, values=None, /, *, as_thread=False, delete=False, **kwargs):
        self.data = {}
        if data:
            self.add(data, values, as_thread=as_thread, delete=delete, **kwargs)

    def add(self, data, values=None, /, *, as_thread=False, delete=False, **kwargs):
        """Adds data to the store.
        - data can be a recordset, in which case the model must have a _to_store() method, with
          optional kwargs passed to it.
        - data can be a model name, in which case values must be a dict or list of dict.
        - data can be a dict, in which case it is considered as values for the Store model.
        - as_thread: whether to call "_thread_to_store" or "_to_store"
        """
        if isinstance(data, models.Model):
            if values is not None:
                assert len(data) == 1, f"expected single record {data} with values: {values}"
                assert not kwargs, f"expected empty kwargs with recordset {data} values: {kwargs}"
                assert not delete, f"deleted not expected for {data} with values: {values}"
            if delete:
                assert len(data) == 1, f"expected single record {data} with delete"
                assert values is None, f"for {data} expected empty value with delete: {values}"
            if as_thread:
                if delete:
                    self.add("mail.thread", {"id": data.id, "model": data._name}, delete=True)
                elif values is not None:
                    self.add("mail.thread", {"id": data.id, "model": data._name, **values})
                else:
                    data._thread_to_store(self, **kwargs)
            else:
                if delete:
                    self.add(data._name, {"id": data.id}, delete=True)
                elif values is not None:
                    self.add(data._name, {"id": data.id, **values})
                else:
                    data._to_store(self, **kwargs)
            return self
        if isinstance(data, dict):
            assert not values, f"expected empty values with dict {data}: {values}"
            assert not kwargs, f"expected empty kwargs with dict {data}: {kwargs}"
            assert not as_thread, f"expected not as_thread with dict {data}: {kwargs}"
            model_name = "Store"
            values = data
        else:
            assert not kwargs, f"expected empty kwargs with model name {data}: {kwargs}"
            assert not as_thread, f"expected not as_thread with model name {data}: {kwargs}"
            model_name = data
        assert isinstance(model_name, str), f"expected str for model name: {model_name}: {values}"
        # skip empty values
        if not values:
            return self
        ids = ids_by_model[model_name]
        # handle singleton model: update single record in place
        if not ids:
            assert isinstance(values, dict), f"expected dict for singleton {model_name}: {values}"
            assert not delete, f"Singleton {model_name} cannot be deleted"
            if model_name not in self.data:
                self.data[model_name] = {}
            self._add_values(values, model_name)
            return self
        # handle model with ids: add or update existing records based on ids
        if model_name not in self.data:
            self.data[model_name] = {}
        if isinstance(values, dict):
            values = [values]
        assert isinstance(values, list), f"expected list for {model_name}: {values}"
        for vals in values:
            assert isinstance(vals, dict), f"expected dict for {model_name}: {vals}"
            for i in ids:
                assert vals.get(i), f"missing id {i} in {model_name}: {vals}"
            index = tuple(vals[i] for i in ids)
            if index not in self.data[model_name]:
                self.data[model_name][index] = {}
            self._add_values(vals, model_name, index)
            if delete:
                self.data[model_name][index]["_DELETE"] = True
            elif "_DELETE" in self.data[model_name][index]:
                del self.data[model_name][index]["_DELETE"]
        return self

    def _add_values(self, values, model_name, index=None):
        """Adds values to the store for a given model name and index."""
        target = self.data[model_name][index] if index else self.data[model_name]
        for key, val in values.items():
            assert key != "_DELETE", f"invalid key {key} in {model_name}: {values}"
            subrecord_kwargs = {}
            if isinstance(val, tuple) and len(val) and val[0] is ONE:
                subrecord, as_thread, only_id, subrecord_kwargs = val[1], val[2], val[3], val[4]
                assert not subrecord or isinstance(
                    subrecord, models.Model
                ), f"expected recordset for one {key}: {subrecord}"
                if subrecord and not only_id:
                    self.add(subrecord, as_thread=as_thread, **subrecord_kwargs)
                target[key] = self.one_id(subrecord, as_thread=as_thread)
            elif isinstance(val, tuple) and len(val) and val[0] is MANY:
                subrecords, mode, as_thread, only_id, subrecords_kwargs = (
                    val[1],
                    val[2],
                    val[3],
                    val[4],
                    val[5],
                )
                assert not subrecords or isinstance(
                    subrecords, models.Model
                ), f"expected recordset for many {key}: {subrecords}"
                assert mode in ["ADD", "DELETE", "REPLACE"], f"invalid mode for many {key}: {mode}"
                if subrecords and not only_id:
                    self.add(subrecords, as_thread=as_thread, **subrecords_kwargs)
                rel_val = self.many_ids(subrecords, mode, as_thread=as_thread)
                target[key] = (
                    target[key] + rel_val if key in target and mode != "REPLACE" else rel_val
                )
            elif isinstance(val, datetime):
                target[key] = fields.Datetime.to_string(val)
            elif isinstance(val, date):
                target[key] = fields.Date.to_string(val)
            else:
                target[key] = val

    def get_result(self):
        """Gets resulting data built from adding all data together."""
        res = {}
        for model_name, records in sorted(self.data.items()):
            if not ids_by_model[model_name]:  # singleton
                res[model_name] = dict(sorted(records.items()))
            else:
                res[model_name] = [dict(sorted(record.items())) for record in records.values()]
        return res

    @staticmethod
    def many(records, mode="REPLACE", /, *, as_thread=False, only_id=False, **kwargs):
        """Flags records to be added to the store in a Many relation.
        - mode: "REPLACE" (default), "ADD", or "DELETE"."""
        return (MANY, records, mode, as_thread, only_id, kwargs)

    @staticmethod
    def one(record, /, *, as_thread=False, only_id=False, **kwargs):
        """Flags a record to be added to the store in a One relation."""
        return (ONE, record, as_thread, only_id, kwargs)

    @staticmethod
    def many_ids(records, mode="REPLACE", /, *, as_thread=False):
        """Converts records to a value suitable for a relation in the store.
        - mode: "REPLACE" (default), "ADD", or "DELETE".

        This method does not add the result to the Store. Calling it manually
        should be avoided. It is kept as a public method until all remaining
        occurences can be removed.
        Using the method ``many(..., only_id=True)`` is preferable."""
        if records._name == "mail.message.reaction":
            res = [
                {"message": message.id, "content": content}
                for (message, content), _ in groupby(records, lambda r: (r.message_id, r.content))
            ]
        else:
            res = [Store.one_id(record, as_thread=as_thread) for record in records]
        if mode == "ADD":
            res = [("ADD", res)]
        elif mode == "DELETE":
            res = [("DELETE", res)]
        return res

    @staticmethod
    def one_id(record, /, *, as_thread=False):
        """Converts a record to a value suitable for a relation in the store.

        This method does not add the result to the Store. Calling it manually
        should be avoided. It is kept as a public method until all remaining
        occurences can be removed.
        Using the method ``many(..., only_id=True)`` is preferable."""
        if not record:
            return False
        if as_thread:
            return {"id": record.id, "model": record._name}
        if record._name == "discuss.channel":
            return {"id": record.id, "model": "discuss.channel"}
        if record._name == "mail.guest":
            return {"id": record.id, "type": "guest"}
        if record._name == "res.partner":
            return {"id": record.id, "type": "partner"}
        return record.id
