from collections import defaultdict
from dateutil.relativedelta import relativedelta

from odoo import Command, _, api, fields, models
from odoo.fields import Domain
from odoo.exceptions import UserError


class ResCompany(models.Model):
    _inherit = "res.company"

    account_stock_journal_id = fields.Many2one('account.journal', string='Stock Journal', check_company=True)

    account_stock_valuation_id = fields.Many2one('account.account', string='Stock Valuation Account', check_company=True)

    account_production_wip_account_id = fields.Many2one('account.account', string='Production WIP Account', check_company=True)
    account_production_wip_overhead_account_id = fields.Many2one('account.account', string='Production WIP Overhead Account', check_company=True)

    inventory_period = fields.Selection(
        string='Inventory Period',
        selection=[
            ('manual', 'Manual'),
            ('daily', 'Daily'),
            ('monthly', 'Monthly'),
        ],
        default='manual',
        required=True)

    inventory_valuation = fields.Selection(
        string='Valuation',
        selection=[
            ('periodic', 'Periodic (at closing)'),
            ('real_time', 'Perpetual (at invoicing)'),
        ],
        default='periodic',
    )

    cost_method = fields.Selection(
        string="Cost Method",
        selection=[
            ('standard', "Standard Price"),
            ('fifo', "First In First Out (FIFO)"),
            ('average', "Average Cost (AVCO)"),
        ],
        default='standard',
        required=True,
    )

    def action_close_stock_valuation(self, at_date=None, auto_post=False):
        self.ensure_one()
        if at_date and isinstance(at_date, str):
            at_date = fields.Date.from_string(at_date)
        last_closing_date = self._get_last_closing_date()
        if at_date and last_closing_date and at_date < fields.Date.to_date(last_closing_date):
            raise UserError(self.env._('It exists closing entries after the selected date. Cancel them before generate an entry prior to them'))
        aml_vals_list = self._action_close_stock_valuation(at_date=at_date)

        if not aml_vals_list:
            # No account moves to create, so nothing to display.
            raise UserError(_("Everything is correctly closed"))
        if not self.account_stock_journal_id:
            raise UserError(self.env._("Please set the Journal for Inventory Valuation in the settings."))
        if not self.account_stock_valuation_id:
            raise UserError(self.env._("Please set the Valuation Account for Inventory Valuation in the settings."))

        moves_vals = {
            'journal_id': self.account_stock_journal_id.id,
            'date': at_date or fields.Date.today(),
            'ref': _('Stock Closing'),
            'line_ids': [Command.create(aml_vals) for aml_vals in aml_vals_list],
        }
        account_move = self.env['account.move'].create(moves_vals)
        self._save_closing_id(account_move.id)
        if auto_post:
            account_move._post()

        return {
            'type': 'ir.actions.act_window',
            'name': _("Journal Items"),
            'res_model': 'account.move',
            'res_id': account_move.id,
            'views': [(False, 'form')],
        }

    def stock_value(self, accounts_by_product=None, at_date=None):
        self.ensure_one()
        value_by_account: dict = defaultdict(float)
        if not accounts_by_product:
            accounts_by_product = self.with_context(prefetch_fields=False)._get_accounts_by_product()
        for product, accounts in accounts_by_product.items():
            account = accounts['valuation']
            product_value = product.with_context(to_date=at_date).total_value
            value_by_account[account] += product_value
        return value_by_account

    def stock_accounting_value(self, accounts_by_product=None, at_date=None):
        self.ensure_one()
        if not accounts_by_product:
            accounts_by_product = self._get_accounts_by_product()
        account_data = defaultdict(float)
        stock_valuation_accounts_ids = set()
        for dummy, accounts in accounts_by_product.items():
            stock_valuation_accounts_ids.add(accounts['valuation'].id)
        stock_valuation_accounts = self.env['account.account'].browse(stock_valuation_accounts_ids)
        domain = Domain([
            ('account_id', 'in', stock_valuation_accounts.ids),
            ('company_id', '=', self.id),
            ('parent_state', '=', 'posted'),
        ])
        if at_date:
            domain = domain & Domain([('date', '<=', at_date)])
        amls_group = self.env['account.move.line']._read_group(domain, ['account_id'], ['balance:sum'])
        for account, balance in amls_group:
            account_data[account] += balance
        return account_data

    def _action_close_stock_valuation(self, at_date=None):
        aml_vals_list = []
        accounts_by_product = self._get_accounts_by_product()

        vals_list = self._get_location_valuation_vals(at_date)
        if vals_list:
            # Needed directly since it will impact the accounting stock valuation.
            aml_vals_list += vals_list

        vals_list = self._get_stock_valuation_account_vals(accounts_by_product, at_date, aml_vals_list)
        if vals_list:
            aml_vals_list += vals_list

        vals_list = self._get_continental_realtime_variation_vals(accounts_by_product, at_date, aml_vals_list)
        if vals_list:
            aml_vals_list += vals_list
        return aml_vals_list

    @api.model
    def _cron_post_stock_valuation(self):
        domain = Domain([('inventory_period', '=', 'daily'), ('inventory_valuation', '!=', 'real_time')])
        if fields.Date.today() == fields.Date.today() + relativedelta(day=31):
            domain = domain & Domain([('inventory_period', '=', 'monthly')])
        companies = self.env['res.company'].search(domain)
        for company in companies:
            company.action_close_stock_valuation(auto_post=True)

    def _get_valuation_product_domain(self):
        return [('is_storable', '=', True)]

    def _get_accounts_by_product(self, products=None):
        if not products:
            products = self.env['product.product'].with_company(self).search(self._get_valuation_product_domain())

        accounts_by_product = {}
        for product in products:
            accounts = product._get_product_accounts()
            accounts_by_product[product] = {
                'valuation': accounts['stock_valuation'],
                'variation': accounts['stock_variation'],
                'expense': accounts['expense'],
            }
        return accounts_by_product

    @api.model
    def _get_extra_balance(self, vals_list=None):
        extra_balance = defaultdict(float)
        if not vals_list:
            return extra_balance
        for vals in vals_list:
            extra_balance[vals['account_id']] += (vals['debit'] - vals['credit'])
        return extra_balance

    def _get_location_valuation_vals(self, at_date=None, location_domain=False):
        location_domain = Domain.AND([
            location_domain or [],
            [('valuation_account_id', '!=', False)],
            [('company_id', '=', self.id)],
        ])
        amls_vals_list = []
        valued_location = self.env['stock.location'].search(location_domain)
        last_closing_date = self._get_last_closing_date()
        moves_base_domain = Domain([
            ('product_id.is_storable', '=', True),
            ('product_id.valuation', '=', 'periodic')
        ])
        if last_closing_date:
            moves_base_domain &= Domain([('date', '>', last_closing_date)])
        if at_date:
            moves_base_domain &= Domain([('date', '<=', at_date)])
        moves_in_domain = Domain([
            ('is_out', '=', True),
            ('company_id', '=', self.id),
            ('location_dest_id', 'in', valued_location.ids),
        ]) & moves_base_domain
        moves_in_by_location = self.env['stock.move']._read_group(
            moves_in_domain,
            ['location_dest_id', 'product_category_id'],
            ['value:sum'],
        )
        moves_out_domain = Domain([
            ('is_in', '=', True),
            ('company_id', '=', self.id),
            ('location_id', 'in', valued_location.ids),
        ]) & moves_base_domain
        moves_out_by_location = self.env['stock.move']._read_group(
            moves_out_domain,
            ['location_id', 'product_category_id'],
            ['value:sum'],
        )
        account_balance = defaultdict(float)
        for location, category, value in moves_in_by_location:
            stock_valuation_acc = category.property_stock_valuation_account_id or self.account_stock_valuation_id
            account_balance[location.valuation_account_id, stock_valuation_acc] += value

        for location, category, value in moves_out_by_location:
            stock_valuation_acc = category.property_stock_valuation_account_id or self.account_stock_valuation_id
            account_balance[location.valuation_account_id, stock_valuation_acc] -= value

        for (location_account, stock_account), balance in account_balance.items():
            if balance == 0:
                continue
            amls_vals = self._prepare_inventory_aml_vals(
                location_account,
                stock_account,
                balance,
                _('Closing: Location Reclassification - [%(account)s]', account=location_account.display_name),
            )
            amls_vals_list += amls_vals
        return amls_vals_list

    def _get_stock_valuation_account_vals(self, accounts_by_product, at_date=None, extra_aml_vals_list=None):
        amls_vals_list = []
        if not accounts_by_product:
            return amls_vals_list

        extra_balance = self._get_extra_balance(extra_aml_vals_list)

        if 'inventory_data' in self.env.context:
            inventory_data = self.env.context.get('inventory_data')
        else:
            inventory_data = self.stock_value(accounts_by_product, at_date)
        accounting_data = self.stock_accounting_value(accounts_by_product, at_date)

        accounts = inventory_data.keys() | accounting_data.keys()
        for account in accounts:
            account_variation = account.account_stock_variation_id
            if not account_variation:
                account_variation = self.expense_account_id
            if not account_variation:
                continue
            balance = inventory_data.get(account, 0) - accounting_data.get(account, 0)
            balance -= extra_balance.get(account.id, 0)

            if self.currency_id.is_zero(balance):
                continue

            amls_vals = self._prepare_inventory_aml_vals(
                account,
                account_variation,
                balance,
                _('Closing: Stock Variation Global for company [%(company)s]', company=self.display_name),
            )
            amls_vals_list += amls_vals

        return amls_vals_list

    def _get_continental_realtime_variation_vals(self, accounts_by_product, at_date=None, extra_aml_vals_list=None):
        """ In continental perpetual the inventory variation is never posted.
        This method compute the variation for a period and post it.
        """
        extra_balance = self._get_extra_balance(extra_aml_vals_list)

        fiscal_year_date_from = self.compute_fiscalyear_dates(fields.Date.today())['date_from']

        amls_vals_list = []
        accounting_data_today = self.stock_accounting_value(accounts_by_product)
        accounting_data_last_period = self.stock_accounting_value(accounts_by_product, at_date=fiscal_year_date_from)

        accounts = accounting_data_today.keys() | accounting_data_last_period.keys()

        for account in accounts:
            variation_acc = account.account_stock_variation_id
            expense_acc = account.account_stock_expense_id

            if not variation_acc or not expense_acc:
                continue

            balance_today = accounting_data_today.get(account, 0) - extra_balance[account]
            balance_last_period = accounting_data_last_period.get(account, 0)
            balance_over_period = balance_today - balance_last_period

            current_balance_domain = Domain([
                ('account_id', '=', variation_acc.id),
                ('company_id', '=', self.id),
                ('parent_state', '=', 'posted'),
            ])
            if at_date:
                current_balance_domain &= Domain([('date', '<=', at_date)])
            existing_balance = sum(self.env['account.move.line'].search(current_balance_domain).mapped('balance'))
            balance_over_period += existing_balance

            if self.currency_id.is_zero(balance_over_period):
                continue

            amls_vals = self._prepare_inventory_aml_vals(
                expense_acc,
                variation_acc,
                balance_over_period,
                _('Closing: Stock Variation Over Period'),
            )
            amls_vals_list += amls_vals

        return amls_vals_list

    def _prepare_inventory_aml_vals(self, debit_acc, credit_acc, balance, ref, product_id=False):
        if balance < 0:
            temp = credit_acc
            credit_acc = debit_acc
            debit_acc = temp
            balance = abs(balance)
        return [{
            'account_id': credit_acc.id,
            'name': ref,
            'debit': 0,
            'credit': balance,
            'product_id': product_id,
        }, {
            'account_id': debit_acc.id,
            'name': ref,
            'debit': balance,
            'credit': 0,
            'product_id': product_id,
        }]

    def _get_last_closing_date(self):
        self.ensure_one()
        key = f'{self.id}.stock_valuation_closing_ids'
        closing_ids = self.env['ir.config_parameter'].sudo().get_param(key)
        closing_ids = closing_ids.split(',') if closing_ids else []
        closing = self.env['account.move']
        while not closing and closing_ids:
            closing_id = closing_ids.pop(-1)
            closing_id = int(closing_id)
            closing = self.env['account.move'].browse(closing_id).exists().filtered(lambda am: am.state == 'posted')
        if not closing:
            return False
        am_state_field = self.env['ir.model.fields'].sudo().search([('model', '=', 'account.move'), ('name', '=', 'state')], limit=1)
        state_tracking = closing.message_ids.sudo().tracking_value_ids.filtered(lambda t: t.field_id == am_state_field).sorted('id')
        create_date = state_tracking[-1:].create_date
        if create_date and create_date.date() == closing.date:
            return create_date
        return fields.Datetime.to_datetime(closing.date)

    def _save_closing_id(self, move_id):
        self.ensure_one()
        key = f'{self.id}.stock_valuation_closing_ids'
        closing_ids = self.env['ir.config_parameter'].sudo().get_param(key)
        ids = closing_ids.split(',') if closing_ids else []
        ids.append(str(move_id))
        if len(ids) > 10:
            ids = ids[1:]
        self.env['ir.config_parameter'].sudo().set_param(key, ','.join(ids))

    def _set_category_defaults(self):
        for company in self:
            self.env['ir.default'].set('product.category', 'property_valuation', company.inventory_valuation, company_id=company.id)
            self.env['ir.default'].set('product.category', 'property_cost_method', company.cost_method, company_id=company.id)
            self.env['ir.default'].set('product.category', 'property_stock_journal', company.account_stock_journal_id.id, company_id=company.id)
            self.env['ir.default'].set('product.category', 'property_stock_valuation_account_id', company.account_stock_valuation_id.id, company_id=company.id)
