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

import base64
import io

from PyPDF2 import PdfFileWriter, PdfFileReader
from PyPDF2.generic import NameObject, createStringObject

from odoo import models
from odoo.tools import format_amount, format_date, format_datetime, pdf


class IrActionsReport(models.Model):
    _inherit = 'ir.actions.report'

    def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None):
        result = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids=res_ids)
        if self._get_report(report_ref).report_name != 'sale.report_saleorder':
            return result

        orders = self.env['sale.order'].browse(res_ids)

        for order in orders:
            initial_stream = result[order.id]['stream']
            if initial_stream:
                order_template = order.sale_order_template_id
                header_record = order_template if order_template.sale_header else order.company_id
                footer_record = order_template if order_template.sale_footer else order.company_id
                has_header = bool(header_record.sale_header)
                has_footer = bool(footer_record.sale_footer)
                included_product_docs = self.env['product.document']
                doc_line_id_mapping = {}
                for line in order.order_line:
                    product_product_docs = line.product_id.product_document_ids
                    product_template_docs = line.product_template_id.product_document_ids
                    doc_to_include = (
                        product_product_docs.filtered(lambda d: d.attached_on == 'inside')
                        or product_template_docs.filtered(lambda d: d.attached_on == 'inside')
                    )
                    included_product_docs = included_product_docs | doc_to_include
                    doc_line_id_mapping.update({doc.id: line.id for doc in doc_to_include})

                if (not has_header and not included_product_docs and not has_footer):
                    continue

                writer = PdfFileWriter()
                if has_header:
                    self._add_pages_to_writer(writer, base64.b64decode(header_record.sale_header))
                if included_product_docs:
                    for doc in included_product_docs:
                        self._add_pages_to_writer(
                            writer, base64.b64decode(doc.datas), doc_line_id_mapping[doc.id]
                        )
                self._add_pages_to_writer(writer, initial_stream.getvalue())
                if has_footer:
                    self._add_pages_to_writer(writer, base64.b64decode(footer_record.sale_footer))

                form_fields = self._get_form_fields_mapping(order, doc_line_id_mapping)
                pdf.fill_form_fields_pdf(writer, form_fields=form_fields)
                with io.BytesIO() as _buffer:
                    writer.write(_buffer)
                    stream = io.BytesIO(_buffer.getvalue())
                result[order.id].update({'stream': stream})

        return result

    def _add_pages_to_writer(self, writer, document, sol_id=None):
        prefix = f'{sol_id}_' if sol_id else ''
        reader = PdfFileReader(io.BytesIO(document), strict=False)
        sol_field_names = self._get_sol_form_fields_names()
        for page_id in range(0, reader.getNumPages()):
            page = reader.getPage(page_id)
            if sol_id and page.get('/Annots'):
                # Prefix all form fields in the document with the sale order line id.
                # This is necessary to avoid conflicts between fields with the same name.
                for j in range(0, len(page['/Annots'])):
                    reader_annot = page['/Annots'][j].getObject()
                    if reader_annot.get('/T') in sol_field_names:
                        reader_annot.update({
                            NameObject("/T"): createStringObject(prefix + reader_annot.get('/T'))
                        })
            writer.addPage(page)

    def _get_sol_form_fields_names(self):
        """ List of specific pdf fields name for an order line that needs to be renamed in the pdf.
        Override this method to add new fields to the list.
        """
        return ['description', 'quantity', 'uom', 'price_unit', 'discount', 'product_sale_price',
                'taxes', 'tax_excl_price', 'tax_incl_price']

    def _get_form_fields_mapping(self, order, doc_line_id_mapping=None):
        """ Dictionary mapping specific pdf fields name to Odoo fields data for a sale order.
        Override this method to add new fields to the mapping.

        :param recordset order: sale.order record
        :rtype: dict
        :return: mapping of fields name to Odoo fields data

        Note: order.ensure_one()
        """
        order.ensure_one()
        env = self.with_context(use_babel=True).env
        tz = order.partner_id.tz or self.env.user.tz or 'UTC'
        lang_code = order.partner_id.lang or self.env.user.lang
        form_fields_mapping = {
            'name': order.name,
            'partner_id__name': order.partner_id.name,
            'user_id__name': order.user_id.name,
            'amount_untaxed': format_amount(env, order.amount_untaxed, order.currency_id),
            'amount_total': format_amount(env, order.amount_total, order.currency_id),
            'delivery_date': format_datetime(env, order.commitment_date, tz=tz),
            'validity_date': format_date(env, order.validity_date, lang_code=lang_code),
            'client_order_ref': order.client_order_ref or '',
        }

        # Adding fields from each line, prefixed by the line_id to avoid conflicts
        lines_with_doc_ids = set(doc_line_id_mapping.values())
        for line in order.order_line.filtered(lambda sol: sol.id in lines_with_doc_ids):
            form_fields_mapping.update(self._get_sol_form_fields_mapping(line))

        return form_fields_mapping

    def _get_sol_form_fields_mapping(self, line):
        """ Dictionary mapping specific pdf fields name to Odoo fields data for a sale order line.

        Fields name are prefixed by the line id to avoid conflict between files.

        Override this method to add new fields to the mapping.

        :param recordset line: sale.order.line record
        :rtype: dict
        :return: mapping of prefixed fields name to Odoo fields data

        Note: line.ensure_one()
        """
        line.ensure_one()
        env = self.with_context(use_babel=True).env
        return {
            f'{line.id}_description': line.name,
            f'{line.id}_quantity': line.product_uom_qty,
            f'{line.id}_uom': line.product_uom.name,
            f'{line.id}_price_unit': format_amount(env, line.price_unit, line.currency_id),
            f'{line.id}_discount': line.discount,
            f'{line.id}_product_sale_price': format_amount(
                env, line.product_id.lst_price, line.product_id.currency_id
            ),
            f'{line.id}_taxes': ', '.join(tax.name for tax in line.tax_id),
            f'{line.id}_tax_excl_price': format_amount(env, line.price_subtotal, line.currency_id),
            f'{line.id}_tax_incl_price': format_amount(env, line.price_total, line.currency_id),
        }
