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

import collections
import logging

import babel.messages.pofile
import werkzeug
import werkzeug.exceptions
from werkzeug.urls import iri_to_uri

from odoo.http import request, router
from odoo.http.session import get_default_session, session_store
from odoo.tools.misc import file_open
from odoo.tools.translate import JAVASCRIPT_TRANSLATION_COMMENT

_logger = logging.getLogger(__name__)


def clean_action(action, env):
    action_type = action.setdefault('type', 'ir.actions.act_window_close')
    if action_type == 'ir.actions.act_window' and not action.get('views'):
        generate_views(action)

    # When returning an action, keep only relevant fields/properties
    readable_fields = env[action['type']]._get_readable_fields()
    action_type_fields = env[action['type']]._fields.keys()

    cleaned_action = {
        field: value
        for field, value in action.items()
        # keep allowed fields and custom properties fields
        if field in readable_fields or field not in action_type_fields
    }

    # Warn about custom properties fields, because use is discouraged
    action_name = action.get('name') or action
    custom_properties = action.keys() - readable_fields - action_type_fields
    if custom_properties:
        _logger.warning("Action %r contains custom properties %s. Passing them "
            "via the `params` or `context` properties is recommended instead",
            action_name, ', '.join(map(repr, custom_properties)))

    return cleaned_action


def ensure_db(redirect='/web/database/selector', db=None):
    # This helper should be used in web client auth="none" routes
    # if those routes needs a db to work with.
    # If the heuristics does not find any database, then the users will
    # be redirected to db selector or any url specified by `redirect`
    # argument. If the db is taken out of a query parameter, it will be
    # checked against `http.router.db_filter()` in order to ensure it's
    # legit and thus avoid db forgering that could lead to xss attacks.
    if db is None:
        db = request.params.get('db') and request.params.get('db').strip()

    # Ensure db is legit
    if db and db not in router.db_filter([db]):
        db = None

    if db and not request.session.db:
        # User asked a specific database on a new session.
        # That mean the nodb router has been used to find the route
        # Depending on installed module in the database, the rendering of the page
        # may depend on data injected by the database route dispatcher.
        # Thus, we redirect the user to the same page but with the session cookie set.
        # This will force using the database route dispatcher...
        r = request.httprequest
        url_redirect = werkzeug.urls.url_parse(r.base_url)
        if r.query_string:
            # in P3, request.query_string is bytes, the rest is text, can't mix them
            query_string = iri_to_uri(r.query_string.decode())
            url_redirect = url_redirect.replace(query=query_string)
        request.session.db = db
        werkzeug.exceptions.abort(request.redirect(url_redirect.to_url(), 302))

    # if db not provided, use the session one
    if not db and request.session.db and router.db_filter([request.session.db]):
        db = request.session.db

    # if no database provided and no database in session, use monodb
    if not db:
        all_dbs = router.db_list(force=True)
        if len(all_dbs) == 1:
            db = all_dbs[0]

    # if no db can be found til here, send to the database selector
    # the database selector will redirect to database manager if needed
    if not db:
        werkzeug.exceptions.abort(request.redirect(redirect, 303))

    # always switch the session to the computed db
    if db != request.session.db:
        request.session = session_store().new()
        request.session.update(get_default_session(), db=db)
        request.session.context['lang'] = request.default_lang()
        werkzeug.exceptions.abort(request.redirect(request.httprequest.url, 302))


# I think generate_views should go into js ActionManager
def generate_views(action):
    """
    While the server generates a sequence called "views" computing dependencies
    between a bunch of stuff for views coming directly from the database
    (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
    to return custom view dictionaries generated on the fly.

    In that case, there is no ``views`` key available on the action.

    Since the web client relies on ``action['views']``, generate it here from
    ``view_mode`` and ``view_id``.

    Currently handles two different cases:

    * no view_id, multiple view_mode
    * single view_id, single view_mode

    :param dict action: action descriptor dictionary to generate a views key for
    """
    view_id = action.get('view_id') or False
    if isinstance(view_id, (list, tuple)):
        view_id = view_id[0]

    # providing at least one view mode is a requirement, not an option
    view_modes = action['view_mode'].split(',')

    if len(view_modes) > 1:
        if view_id:
            raise ValueError('Non-db action dictionaries should provide '
                             'either multiple view modes or a single view '
                             'mode and an optional view id.\n\n Got view '
                             'modes %r and view id %r for action %r' % (
                view_modes, view_id, action))
        action['views'] = [(False, mode) for mode in view_modes]
        return
    action['views'] = [(view_id, view_modes[0])]


def get_action(env, path_part):
    """
    Get a ir.actions.actions() given an action typically found in a
    "/odoo"-like url.

    The action can take one of the following forms:
    * "action-" followed by a record id
    * "action-" followed by a xmlid
    * "m-" followed by a model name (act_window's res_model)
    * a dotted model name (act_window's res_model)
    * a path (ir.action's path)
    """
    Actions = env['ir.actions.actions']

    if path_part.startswith('action-'):
        someid = path_part.removeprefix('action-')
        if someid.isdigit():  # record id
            action = Actions.sudo().browse(int(someid)).exists()
        elif '.' in someid:   # xml id
            action = env.ref(someid, False)
            if not action or not action._name.startswith('ir.actions'):
                action = Actions
        else:
            action = Actions
    elif path_part.startswith('m-') or '.' in path_part:
        model = path_part.removeprefix('m-')
        if model in env and not env[model]._abstract:
            action = env['ir.actions.act_window'].sudo().search([
                ('res_model', '=', model)], limit=1)
            if not action:
                action = env['ir.actions.act_window'].new(
                    env[model].get_formview_action()
                )
        else:
            action = Actions
    else:
        action = Actions.sudo().search([('path', '=', path_part)])

    if action and action._name == 'ir.actions.actions':
        action_type = action.read(['type'])[0]['type']
        action = env[action_type].browse(action.id)

    return action


def get_action_triples(env, path, *, start_pos=0):
    """
    Extract the triples (active_id, action, record_id) from a "/odoo"-like path.

    >>> env = ...
    >>> list(get_action_triples(env, "/all-tasks/5/project.project/1/tasks"))
    [
        # active_id, action,                     record_id
        ( None,      ir.actions.act_window(...), 5         ), # all-tasks
        ( 5,         ir.actions.act_window(...), 1         ), # project.project
        ( 1,         ir.actions.act_window(...), None      ), # tasks
    ]
    """
    parts = collections.deque(path.strip('/').split('/'))
    active_id = None
    record_id = None

    while parts:
        if not parts:
            e = "expected action at word {} but found nothing"
            raise ValueError(e.format(path.count('/') + start_pos))
        action_name = parts.popleft()
        action = get_action(env, action_name)
        if not action:
            e = f"expected action at word {{}} but found “{action_name}”"
            raise ValueError(e.format(path.count('/') - len(parts) + start_pos))

        record_id = None
        if parts:
            if parts[0] == 'new':
                parts.popleft()
                record_id = None
            elif parts[0].isdigit():
                record_id = int(parts.popleft())

        yield (active_id, action, record_id)

        if len(parts) > 1 and parts[0].isdigit():  # new active id
            active_id = int(parts.popleft())
        elif record_id:
            active_id = record_id


def _get_login_redirect_url(uid, redirect=None):
    """ Decide if user requires a specific post-login redirect, e.g. for 2FA, or if they are
    fully logged and can proceed to the requested URL
    """
    if request.session.uid:  # fully logged
        return redirect or ('/odoo' if is_user_internal(request.session.uid)
                            else '/web/login_successful')

    # partial session (MFA)
    url = request.env(user=uid)['res.users'].browse(uid)._mfa_url()
    if not redirect:
        return url

    parsed = werkzeug.urls.url_parse(url)
    qs = parsed.decode_query()
    qs['redirect'] = redirect
    return parsed.replace(query=werkzeug.urls.url_encode(qs)).to_url()


def is_user_internal(uid):
    return request.env['res.users'].browse(uid)._is_internal()


def _local_web_translations(trans_file):
    messages = []
    try:
        with file_open(trans_file, filter_ext=('.po')) as t_file:
            po = babel.messages.pofile.read_po(t_file)
    except Exception:
        return
    for x in po:
        if x.id and x.string and JAVASCRIPT_TRANSLATION_COMMENT in x.auto_comments:
            messages.append({'id': x.id, 'string': x.string})
    return messages
