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

from collections import OrderedDict
from urllib3.util import parse_url

from odoo import models
from odoo.http import request
from odoo.tools import lazy
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.osv import expression
from odoo.addons.website.models import ir_http
from odoo.exceptions import AccessError


_logger = logging.getLogger(__name__)
re_background_image = re.compile(r"(background-image\s*:\s*url\(\s*['\"]?\s*)([^)'\"]+)")


class IrQWeb(models.AbstractModel):
    """ IrQWeb object for rendering stuff in the website context """

    _inherit = 'ir.qweb'

    URL_ATTRS = {
        'form': 'action',
        'a': 'href',
        'link': 'href',
        'script': 'src',
        'img': 'src',
    }

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_compile``. """
        return super()._get_template_cache_keys() + ['website_id', 'cookies_allowed']

    def _prepare_frontend_environment(self, values):
        """ Update the values and context with website specific value
            (required to render website layout template)
        """
        irQweb = super()._prepare_frontend_environment(values)

        current_website = request.website
        editable = irQweb.env.user.has_group('website.group_website_designer')
        has_group_restricted_editor = irQweb.env.user.has_group('website.group_website_restricted_editor')
        if not editable and has_group_restricted_editor and 'main_object' in values:
            try:
                main_object = values['main_object'].with_user(irQweb.env.user.id)
                current_website._check_user_can_modify(main_object)
                editable = True
            except AccessError:
                pass
        translatable = has_group_restricted_editor and irQweb.env.context.get('lang') != irQweb.env['ir.http']._get_default_lang().code
        editable = editable and not translatable

        if has_group_restricted_editor and irQweb.env.user.has_group('website.group_multi_website'):
            values['multi_website_websites_current'] = lazy(lambda: current_website.name)
            values['multi_website_websites'] = lazy(lambda: [
                {'website_id': website.id, 'name': website.name, 'domain': website.domain}
                for website in current_website.search([('id', '!=', current_website.id)])
            ])

            cur_company = irQweb.env.company
            values['multi_website_companies_current'] = lazy(lambda: {'company_id': cur_company.id, 'name': cur_company.name})
            values['multi_website_companies'] = lazy(lambda: [
                {'company_id': comp.id, 'name': comp.name}
                for comp in irQweb.env.user.company_ids if comp != cur_company
            ])

        # update values

        values.update(dict(
            website=current_website,
            is_view_active=lazy(lambda: current_website.is_view_active),
            res_company=lazy(request.env['res.company'].browse(current_website._get_cached('company_id')).sudo),
            translatable=translatable,
            editable=editable,
        ))

        if editable:
            # form editable object, add the backend configuration link
            if 'main_object' in values and has_group_restricted_editor:
                func = getattr(values['main_object'], 'get_backend_menu_id', False)
                values['backend_menu_id'] = lazy(lambda: func and func() or irQweb.env['ir.model.data']._xmlid_to_res_id('website.menu_website_configuration'))

        # update options

        irQweb = irQweb.with_context(website_id=current_website.id)
        if 'inherit_branding' not in irQweb.env.context and not self.env.context.get('rendering_bundle'):
            if editable:
                # in edit mode add branding on ir.ui.view tag nodes
                irQweb = irQweb.with_context(inherit_branding=True)
            elif has_group_restricted_editor:
                # will add the branding on fields (into values)
                irQweb = irQweb.with_context(inherit_branding_auto=True)

        # Avoid cache inconsistencies: if the cookies have been accepted, the
        # DOM structure should reflect it after a reload and not be stuck in its
        # previous state (see the part related to cookies in
        # `_post_processing_att`).
        is_allowed_optional_cookies = request.env['ir.http']._is_allowed_cookie('optional')
        irQweb = irQweb.with_context(cookies_allowed=is_allowed_optional_cookies)

        return irQweb

    def _post_processing_att(self, tagName, atts):
        if atts.get('data-no-post-process'):
            return atts

        atts = super()._post_processing_att(tagName, atts)

        website = ir_http.get_request_website()
        if not website and self.env.context.get('website_id'):
            website = self.env['website'].browse(self.env.context['website_id'])
        if website and tagName == 'img' and 'loading' not in atts:
            atts['loading'] = 'lazy'  # default is auto

        if self.env.context.get('inherit_branding') or self.env.context.get('rendering_bundle') or \
           self.env.context.get('edit_translations') or self.env.context.get('debug') or (request and request.session.debug):
            return atts

        if not website:
            return atts

        if (
            website.cookies_bar
            and website.block_third_party_domains
            and not self.env.context.get('cookies_allowed')
            and not request.env.user.has_group('website.group_website_restricted_editor')
        ):
            # If the cookie banner is activated, 3rd-party embedded iframes and
            # scripts should be controlled. As such:
            # - 'domains' is a watchlist on the iframe/script's src itself,
            # - 'classes' is a watchlist on container elements in which iframes
            # are/could be built on the fly client-side for some reason.
            cookies_watchlist = {
                'domains': website.blocked_third_party_domains.split('\n'),
                'classes': website._get_blocked_iframe_containers_classes(),
            }
            remove_src = False
            if tagName in ('iframe', 'script'):
                src_host = parse_url((atts.get('src') or '').lower()).host
                if src_host:
                    remove_src = any(
                        # "www.example.com" and "example.com" should block both.
                        src_host == domain.removeprefix('www.')
                        # "domain.com" should block "subdomain.domain.com", but
                        # not "(subdomain.)mydomain.com".
                        or src_host.endswith('.' + domain.removeprefix('www.'))
                        for domain in cookies_watchlist['domains']
                    )
            if (
                remove_src
                or cookies_watchlist['classes'].intersection((atts.get('class') or '').split(' '))
            ):
                atts['data-need-cookies-approval'] = 'true'
                # Case class in watchlist: we stop here. The element could
                # contain an iframe created on the fly client-side. It is marked
                # now so that the iframe can be marked later when created.
                # Case iframe/script's src in watchlist: we adapt the src.
                if 'src' in atts:
                    atts['data-nocookie-src'] = atts['src']
                    atts['src'] = 'about:blank'

        name = self.URL_ATTRS.get(tagName)
        if request:
            value = atts.get(name) if name else None
            if value is not None and value is not False:
                atts[name] = self.env['ir.http']._url_for(str(value))

            # Adapt background-image URL in the same way as image src.
            atts = self._adapt_style_background_image(atts, self.env['ir.http']._url_for)

        if not website.cdn_activated:
            return atts

        data_name = f'data-{name}'
        if name and (name in atts or data_name in atts):
            atts = OrderedDict(atts)
            if name in atts:
                atts[name] = website.get_cdn_url(atts[name])
            if data_name in atts:
                atts[data_name] = website.get_cdn_url(atts[data_name])
        atts = self._adapt_style_background_image(atts, website.get_cdn_url)

        return atts

    def _adapt_style_background_image(self, atts, url_adapter):
        if isinstance(atts.get('style'), str) and 'background-image' in atts['style']:
            atts['style'] = re_background_image.sub(lambda m: '%s%s' % (m[1], url_adapter(m[2])), atts['style'])
        return atts

    def _get_bundles_to_pregenarate(self):
        js_assets, css_assets = super(IrQWeb, self)._get_bundles_to_pregenarate()
        assets = {
            'website.backend_assets_all_wysiwyg',
            'website.assets_all_wysiwyg',
        }
        return (js_assets | assets, css_assets | assets)
