from odoo import http
from odoo.fields import Domain
from odoo.http import request
from odoo.service.model import call_kw
from werkzeug.exceptions import Forbidden, NotFound, BadRequest, Unauthorized
from odoo.exceptions import MissingError
from odoo.tools import consteq


class PosSelfOrderController(http.Controller):
    @http.route("/pos-self-order/process-order/<device_type>/", auth="public", type="jsonrpc", website=True)
    def process_order(self, order, access_token, table_identifier, device_type):
        pos_config, table = self._verify_authorization(access_token, table_identifier, order)
        if not pos_config.self_ordering_mode == device_type:
            raise Unauthorized("Invalid device type")

        # Create a safe copy of the order with only the necessary fields for order creation to
        # avoid potential security issues and to reduce the payload size
        safe_data = pos_config.env['pos.order']._check_pos_order(pos_config, order, device_type, table)
        results = pos_config.env['pos.order'].sudo().with_company(pos_config.company_id.id).sync_from_ui([safe_data])
        order_ids = pos_config.env['pos.order'].browse([order['id'] for order in results['pos.order']])

        # Recompute all prices from newly created lines to ensure price correctness and
        # avoid potential manipulation from the frontend
        order_ids.recompute_prices()
        amount_total = order_ids.amount_total

        if amount_total == 0:
            order_ids.state = 'paid'
            order_ids._process_saved_order(False)
            order_ids._send_self_order_receipt()

        return self._generate_return_values(order_ids, pos_config)

    def _generate_return_values(self, order, config):
        orders = self.env['pos.order']._load_pos_self_data_read(order, config)

        for o in orders:
            del o['email']
            del o['mobile']

        result = {
            'pos.order': orders,
            'pos.order.line': self.env['pos.order.line']._load_pos_self_data_read(order.lines, config),
            'pos.payment': self.env['pos.payment']._load_pos_self_data_read(order.payment_ids, config),
            'product.attribute.custom.value': self.env['product.attribute.custom.value']._load_pos_self_data_read(order.lines.custom_attribute_value_ids, config),
        }
        if config.self_ordering_mode == 'mobile':
            result['pos.payment.method'] = self.env['pos.payment.method']._load_pos_self_data_read(order.payment_ids.payment_method_id, config)
        return result

    def _verify_line_price(self, lines, pos_config, preset_id):
        lines.order_id.recompute_prices()

    @http.route('/pos-self-order/validate-partner', auth='public', type='jsonrpc', website=True)
    def validate_partner(self, access_token, name, phone, street, zip, city, country_id, state_id=None, partner_id=None, email=None):
        pos_config = self._verify_pos_config(access_token)
        existing_partner = pos_config.env['res.partner'].sudo().browse(int(partner_id)) if partner_id else False

        if existing_partner and existing_partner.exists():
            return {
                'res.partner': existing_partner.read(['id'], load=False),
            }

        state_id = pos_config.env['res.country.state'].browse(int(state_id)) if state_id else False
        country_id = pos_config.env['res.country'].browse(int(country_id))
        partner_sudo = request.env['res.partner'].sudo().create({
            'name': name,
            'email': email,
            'phone': phone,
            'street': street,
            'zip': zip,
            'city': city,
            'country_id': country_id.id,
            'state_id': state_id.id if state_id else False,
            'company_id': pos_config.company_id.id,
        })

        return {
            'res.partner': partner_sudo.read(['id'], load=False),
        }

    @http.route('/pos-self-order/remove-order', auth='public', type='jsonrpc', website=True)
    def remove_order(self, access_token, order_id, order_access_token):
        pos_config = self._verify_pos_config(access_token)
        pos_order = pos_config.env['pos.order'].browse(order_id)

        if not pos_order.exists() or not consteq(pos_order.access_token, order_access_token):
            raise MissingError(self.env._("Your order does not exist or has been removed"))

        if pos_order.state != 'draft':
            raise Unauthorized(self.env._("You are not authorized to remove this order"))

        pos_order.remove_from_ui([pos_order.id])

    @http.route('/pos-self-order/get-user-data', auth='public', type='jsonrpc', website=True)
    def get_orders_by_access_token(self, access_token, order_access_tokens, table_identifier=None):
        pos_config = self._verify_pos_config(access_token)
        domain = [(False, '=', True)]

        for data in order_access_tokens:
            domain = Domain.OR([domain, ['&',
                ('access_token', '=', data['access_token']),
                '|',
                ('write_date', '>', data.get('write_date')),
                ('state', '!=', data.get('state')),
            ]])
        orders = pos_config.env['pos.order'].search(domain)
        access_tokens = set({o.get('access_token') for o in order_access_tokens})
        # Do not use session.order_ids, it may fail if there is shared sessions
        existing_order_tokens = pos_config.env['pos.order'].search([('access_token', 'in', access_tokens)]).mapped('access_token')
        if deleted_order_tokens := list(access_tokens - set(existing_order_tokens)):
            # Remove orders that no longer exist on the server but are still shown in the self-order UI
            pos_config._notify('REMOVE_ORDERS', {'deleted_order_tokens': deleted_order_tokens})
        return self._generate_return_values(orders, pos_config) if orders else {}

    @http.route('/pos-self-order/update-last-changes', auth='public', type='jsonrpc', website=True)
    def update_last_changes(self, access_token, order_id, order_access_token, last_order_preparation_change=None):
        """
        This route can be used to update the last_order_preparation_change
        field of the order or to simply retrieve the current value
        """
        pos_config = self._verify_pos_config(access_token)
        pos_order = pos_config.env['pos.order'].browse(order_id)

        if not pos_order.exists() or not consteq(pos_order.access_token, order_access_token):
            raise MissingError(self.env._("Your order does not exist or has been removed"))

        if last_order_preparation_change:
            pos_order.write({'last_order_preparation_change': last_order_preparation_change})

        return {'last_order_preparation_change': pos_order.last_order_preparation_change}

    @http.route('/kiosk/payment/<int:pos_config_id>/<device_type>', auth='public', type='jsonrpc', website=True)
    def pos_self_order_kiosk_payment(self, pos_config_id, order, payment_method_id, access_token, device_type):
        pos_config = self._verify_pos_config(access_token)
        results = self.process_order(order, access_token, None, device_type)

        if not results['pos.order'][0].get('id'):
            raise BadRequest("Something went wrong")

        # access_token verified in process_new_order
        order_sudo = pos_config.env['pos.order'].browse(results['pos.order'][0]['id'])
        payment_method_sudo = pos_config.env["pos.payment.method"].browse(payment_method_id)
        if not order_sudo or not payment_method_sudo or payment_method_sudo not in order_sudo.config_id.payment_method_ids:
            raise NotFound("Order or payment method not found")

        status = payment_method_sudo._payment_request_from_kiosk(order_sudo)

        if not status:
            raise BadRequest("Something went wrong")

        return {'order': self.env['pos.order']._load_pos_self_data_read(order_sudo, pos_config), 'payment_status': status}

    @http.route("/kiosk/payment_method_action/<action>", auth="public", type="jsonrpc", website=True)
    def pos_self_order_kiosk_payment_method_action(self, access_token, action, args, kwargs):
        pos_config = self._verify_pos_config(access_token)
        payment_method_env = pos_config.env["pos.payment.method"]
        if pos_config.self_ordering_mode != "kiosk":
            raise Forbidden("Method only allowed in kiosk mode")
        if args and isinstance(args[0], list):
            if len(args[0]) != 1:
                raise BadRequest("Only one payment method ID should be provided")
            if not payment_method_env.search_count([("id", "=", args[0]), ("config_ids", "in", pos_config.id)]):
                raise NotFound("Payment method not found in config")
        if action not in payment_method_env._allowed_actions_in_self_order():
            raise Forbidden(f"Method '{action}' is forbidden in the self order kiosk")

        return call_kw(payment_method_env, action, args, kwargs)

    @http.route('/pos_self_order/kiosk/increment_nb_print/', auth='public', type='jsonrpc', website=True)
    def pos_kiosk_increment_nb_print(self, access_token, order_id, order_access_token):
        pos_config = self._verify_pos_config(access_token)
        pos_order = pos_config.env['pos.order'].browse(order_id)

        if not pos_order.exists() or not consteq(pos_order.access_token, order_access_token):
            raise MissingError(self.env._("Your order does not exist or has been removed"))

        pos_order.write({
            'nb_print': 1,
        })

    @http.route('/pos-self-order/change-printer-status', auth='public', type='jsonrpc', website=True)
    def change_printer_status(self, access_token, has_paper):
        pos_config = self._verify_pos_config(access_token)
        if has_paper != pos_config.has_paper:
            pos_config.write({'has_paper': has_paper})

    @http.route('/pos-self-order/get-slots', auth='public', type='jsonrpc', website=True)
    def get_slots(self, access_token, preset_id):
        pos_config = self._verify_pos_config(access_token)
        preset = pos_config.env['pos.preset'].browse(preset_id)
        return preset.get_available_slots()

    def _get_order_prices(self, lines):
        amount_untaxed = sum(lines.mapped('price_subtotal'))
        amount_total = sum(lines.mapped('price_subtotal_incl'))
        return amount_total, amount_untaxed

    def _verify_pos_config(self, access_token, check_active_session=True):
        """
        Finds the pos.config with the given access_token and returns a record with reduced privileges.
        The record is has no sudo access and is in the context of the record's company and current pos.session's user.
        """
        pos_config_sudo = request.env['pos.config'].sudo().search([('access_token', '=', access_token)], limit=1)
        if self._verify_config_constraint(pos_config_sudo, check_active_session):
            raise Unauthorized("Invalid access token")
        company = pos_config_sudo.company_id
        user = pos_config_sudo.self_ordering_default_user_id
        return pos_config_sudo.sudo(False).with_company(company).with_user(user).with_context(allowed_company_ids=company.ids)

    def _verify_config_constraint(self, pos_config_sudo, check_active_session=True):
        return not pos_config_sudo or (pos_config_sudo.self_ordering_mode != 'mobile' and pos_config_sudo.self_ordering_mode != 'kiosk') or (check_active_session and not pos_config_sudo.has_active_session)

    def _verify_authorization(self, access_token, table_identifier, order):
        """
        Similar to _verify_pos_config but also looks for the restaurant.table of the given identifier.
        The restaurant.table record is also returned with reduced privileges.
        """
        pos_config = self._verify_pos_config(access_token)
        table_sudo = request.env["restaurant.table"].sudo().search([('identifier', '=', table_identifier)], limit=1)
        preset = request.env['pos.preset'].sudo().browse(order.get('preset_id'))
        is_takeaway = order and pos_config.use_presets and preset and preset.service_at != 'table'
        if not table_sudo and not pos_config.self_ordering_mode == 'kiosk' and pos_config.self_ordering_service_mode == 'table' and not is_takeaway:
            raise Unauthorized("Table not found")

        company = pos_config.company_id
        user = pos_config.self_ordering_default_user_id
        table = table_sudo.sudo(False).with_company(company).with_user(user).with_context(allowed_company_ids=company.ids)
        return pos_config, table

    @http.route(['/pos-self/ping'], type='jsonrpc', auth='public')
    def pos_ping(self, access_token):
        self._verify_pos_config(access_token, check_active_session=False)
        return {'response': 'pong'}
