# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import logging
from datetime import datetime, timedelta
from collections import defaultdict

from odoo import models
from odoo.tools import populate, OrderedSet
from odoo.addons.stock.populate.stock import COMPANY_NB_WITH_STOCK

_logger = logging.getLogger(__name__)


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

    def _populate_factories(self):
        return super()._populate_factories() + [
            ('manufacturing_lead', populate.randint(0, 2)),
        ]


class Warehouse(models.Model):
    _inherit = 'stock.warehouse'

    def _populate_factories(self):
        return super()._populate_factories() + [
            ('manufacture_steps', populate.iterate(['mrp_one_step', 'pbm', 'pbm_sam'], [0.6, 0.2, 0.2]))
        ]


# TODO : stock picking type manufacturing


class MrpBom(models.Model):
    _inherit = 'mrp.bom'

    _populate_sizes = {'small': 100, 'medium': 2_000, 'large': 20_000}
    _populate_dependencies = ['product.product', 'stock.location']

    def _populate_factories(self):
        company_ids = self.env.registry.populated_models['res.company'][:COMPANY_NB_WITH_STOCK]

        product_tmpl_ids = self.env['product.product'].search([
            ('id', 'in', self.env.registry.populated_models['product.product']),
            ('type', 'in', ('product', 'consu'))
        ]).product_tmpl_id.ids
        # Use only a 80 % subset of the products - the 20 % remaining will leaves of the bom tree
        random = populate.Random('subset_product_bom')
        product_tmpl_ids = random.sample(product_tmpl_ids, int(len(product_tmpl_ids) * 0.8))

        def get_product_id(values=None, random=None, **kwargs):
            if random.random() > 0.5:  # 50 % change to target specific product.product
                return False
            return random.choice(self.env['product.template'].browse(values['product_tmpl_id']).product_variant_ids.ids)

        return [
            ('company_id', populate.randomize(
                [False] + company_ids,
                [0.9] + [0.1 / (len(company_ids) or 1.0)] * (len(company_ids))  # TODO: Inverse the weight, but need to make the bom tree by company (in bom line populate)
            )),
            ('product_tmpl_id', populate.randomize(product_tmpl_ids)),
            ('product_id', populate.compute(get_product_id)),
            ('product_qty', populate.randint(1, 5)),
            ('sequence', populate.randint(1, 1000)),
            ('code', populate.constant("R{counter}")),
            ('ready_to_produce', populate.randomize(['all_available', 'asap'])),
            ('produce_delay', populate.randint(1, 4)),
        ]


class MrpBomLine(models.Model):
    _inherit = 'mrp.bom.line'

    _populate_sizes = {'small': 500, 'medium': 10_000, 'large': 100_000}
    _populate_dependencies = ['mrp.bom']

    def _populate_factories(self):
        # TODO: tree of product by company to be more closer to the reality
        boms = self.env['mrp.bom'].search([('id', 'in', self.env.registry.populated_models['mrp.bom'])], order='sequence, product_id, id')

        product_manu_ids = OrderedSet()
        for bom in boms:
            if bom.product_id:
                product_manu_ids.add(bom.product_id.id)
            else:
                for product_id in bom.product_tmpl_id.product_variant_ids:
                    product_manu_ids.add(product_id.id)
        product_manu_ids = list(product_manu_ids)
        product_manu = self.env['product.product'].browse(product_manu_ids)
        # product_no_manu is products which don't have any bom (leaves in the BoM trees)
        product_no_manu = self.env['product.product'].browse(self.env.registry.populated_models['product.product']) - product_manu
        product_no_manu_ids = product_no_manu.ids

        def get_product_id(values, counter, random):
            bom = self.env['mrp.bom'].browse(values['bom_id'])
            last_product_bom = bom.product_id if bom.product_id else bom.product_tmpl_id.product_variant_ids[-1]
            # TODO: index in list is in O(n) can be avoid by a cache dict (if performance issue)
            index_prod = product_manu_ids.index(last_product_bom.id)
            # Always choose a product futher in the recordset `product_manu` to avoid any loops
            # Or used a product in the `product_no_manu`

            sparsity = 0.4  # Increase the sparsity will decrease the density of the BoM trees => smaller Tree

            len_remaining_manu = len(product_manu_ids) - index_prod - 1
            len_no_manu = len(product_no_manu_ids)
            threshold = len_remaining_manu / (len_remaining_manu + sparsity * len_no_manu)
            if random.random() <= threshold:
                # TODO: avoid copy the list (if performance issue)
                return random.choice(product_manu_ids[index_prod+1:])
            else:
                return random.choice(product_no_manu_ids)

        def get_product_uom_id(values, counter, random):
            return self.env['product.product'].browse(values['product_id']).uom_id.id

        return [
            ('bom_id', populate.iterate(boms.ids)),
            ('sequence', populate.randint(1, 1000)),
            ('product_id', populate.compute(get_product_id)),
            ('product_uom_id', populate.compute(get_product_uom_id)),
            ('product_qty', populate.randint(1, 10)),
        ]


class MrpWorkcenter(models.Model):
    _inherit = 'mrp.workcenter'

    _populate_sizes = {'small': 20, 'medium': 100, 'large': 1_000}

    def _populate(self, size):
        res = super()._populate(size)

        # Set alternative workcenters
        _logger.info("Set alternative workcenters")
        # Valid workcenters by company_id (the workcenter without company can be the alternative of all workcenter)
        workcenters_by_company = defaultdict(OrderedSet)
        for workcenter in res:
            workcenters_by_company[workcenter.company_id.id].add(workcenter.id)
        workcenters_by_company = {company_id: self.env['mrp.workcenter'].browse(workcenters) for company_id, workcenters in workcenters_by_company.items()}
        workcenters_by_company = {
            company_id: workcenters | workcenters_by_company.get(False, self.env['mrp.workcenter'])
            for company_id, workcenters in workcenters_by_company.items()}

        random = populate.Random('set_alternative_workcenter')
        for workcenter in res:
            nb_alternative = max(random.randint(0, 3), len(workcenters_by_company[workcenter.company_id.id]) - 1)
            if nb_alternative > 0:
                alternatives_workcenter_ids = random.sample((workcenters_by_company[workcenter.company_id.id] - workcenter).ids, nb_alternative)
                workcenter.alternative_workcenter_ids = [(6, 0, alternatives_workcenter_ids)]

        return res

    def _populate_factories(self):
        company_ids = self.env.registry.populated_models['res.company'][:COMPANY_NB_WITH_STOCK]
        resource_calendar_no_company = self.env.ref('resource.resource_calendar_std').copy({'company_id': False})

        def get_resource_calendar_id(values, counter, random):
            if not values['company_id']:
                return resource_calendar_no_company.id
            return self.env['res.company'].browse(values['company_id']).resource_calendar_id.id

        return [
            ('name', populate.constant("Workcenter - {counter}")),
            ('company_id', populate.iterate(company_ids + [False])),
            ('resource_calendar_id', populate.compute(get_resource_calendar_id)),
            ('active', populate.iterate([True, False], [0.9, 0.1])),
            ('code', populate.constant("W/{counter}")),
            ('default_capacity', populate.iterate([0.5, 1.0, 2.0, 5.0], [0.2, 0.4, 0.2, 0.2])),
            ('sequence', populate.randint(1, 1000)),
            ('color', populate.randint(1, 12)),
            ('costs_hour', populate.randint(5, 25)),
            ('time_start', populate.iterate([0.0, 2.0, 10.0], [0.6, 0.2, 0.2])),
            ('time_stop', populate.iterate([0.0, 2.0, 10.0], [0.6, 0.2, 0.2])),
            ('oee_target', populate.randint(80, 99)),
        ]


class MrpRoutingWorkcenter(models.Model):
    _inherit = 'mrp.routing.workcenter'

    _populate_sizes = {'small': 500, 'medium': 5_000, 'large': 50_000}
    _populate_dependencies = ['mrp.workcenter', 'mrp.bom']

    def _populate_factories(self):
        # Take a subset (70%) of bom to have some of then without any operation
        random = populate.Random('operation_subset_bom')
        boms_ids = self.env.registry.populated_models['mrp.bom']
        boms_ids = random.sample(boms_ids, int(len(boms_ids) * 0.7))

        # Valid workcenters by company_id (the workcenter without company can be used by any operation)
        workcenters_by_company = defaultdict(OrderedSet)
        for workcenter in self.env['mrp.workcenter'].browse(self.env.registry.populated_models['mrp.workcenter']):
            workcenters_by_company[workcenter.company_id.id].add(workcenter.id)
        workcenters_by_company = {company_id: self.env['mrp.workcenter'].browse(workcenters) for company_id, workcenters in workcenters_by_company.items()}
        workcenters_by_company = {
            company_id: workcenters | workcenters_by_company.get(False, self.env['mrp.workcenter'])
            for company_id, workcenters in workcenters_by_company.items()}

        def get_company_id(values, counter, random):
            bom = self.env['mrp.bom'].browse(values['bom_id'])
            return bom.company_id.id

        def get_workcenter_id(values, counter, random):
            return random.choice(workcenters_by_company[values['company_id']]).id

        return [
            ('bom_id', populate.iterate(boms_ids)),
            ('company_id', populate.compute(get_company_id)),
            ('workcenter_id', populate.compute(get_workcenter_id)),
            ('name', populate.constant("OP-{counter}")),
            ('sequence', populate.randint(1, 1000)),
            ('time_mode', populate.iterate(['auto', 'manual'])),
            ('time_mode_batch', populate.randint(1, 100)),
            ('time_cycle_manual', populate.randomize([1.0, 15.0, 60.0, 1440.0])),
        ]


class MrpBomByproduct(models.Model):
    _inherit = 'mrp.bom.byproduct'

    _populate_sizes = {'small': 50, 'medium': 1_000, 'large': 5_000}
    _populate_dependencies = ['mrp.bom.line', 'mrp.routing.workcenter']

    def _populate_factories(self):
        # Take a subset (50%) of bom to have some of then without any operation
        random = populate.Random('byproduct_subset_bom')
        boms_ids = self.env.registry.populated_models['mrp.bom']
        boms_ids = random.sample(boms_ids, int(len(boms_ids) * 0.5))

        boms = self.env['mrp.bom'].search([('id', 'in', self.env.registry.populated_models['mrp.bom'])], order='sequence, product_id, id')

        product_manu_ids = OrderedSet()
        for bom in boms:
            if bom.product_id:
                product_manu_ids.add(bom.product_id.id)
            else:
                for product_id in bom.product_tmpl_id.product_variant_ids:
                    product_manu_ids.add(product_id.id)
        product_manu = self.env['product.product'].browse(product_manu_ids)
        # product_no_manu is products which don't have any bom (leaves in the BoM trees)
        product_no_manu = self.env['product.product'].browse(self.env.registry.populated_models['product.product']) - product_manu
        product_no_manu_ids = product_no_manu.ids

        def get_product_uom_id(values, counter, random):
            return self.env['product.product'].browse(values['product_id']).uom_id.id

        return [
            ('bom_id', populate.iterate(boms_ids)),
            ('product_id', populate.randomize(product_no_manu_ids)),
            ('product_uom_id', populate.compute(get_product_uom_id)),
            ('product_qty', populate.randint(1, 10)),
        ]


class MrpProduction(models.Model):
    _inherit = 'mrp.production'

    _populate_sizes = {'small': 100, 'medium': 1_000, 'large': 10_000}
    _populate_dependencies = ['mrp.routing.workcenter', 'mrp.bom.line']

    def _populate(self, size):
        productions = super()._populate(size)

        def confirm_bom_mo(sample_ratio):
            # Confirm X % of prototype MO
            random = populate.Random('confirm_bom_mo')
            mo_ids = productions.filtered('bom_id').ids
            mo_to_confirm = self.env['mrp.production'].browse(random.sample(mo_ids, int(len(mo_ids) * 0.8)))
            _logger.info("Confirm %d MO with BoM" % len(mo_to_confirm))
            mo_to_confirm.action_confirm()

        # Uncomment this line to confirm a part of MO, can be useful to check performance
        # confirm_bom_mo(0.8)

        return productions

    def _populate_factories(self):
        now = datetime.now()
        company_ids = self.env.registry.populated_models['res.company'][:COMPANY_NB_WITH_STOCK]

        products = self.env['product.product'].browse(self.env.registry.populated_models['product.product'])
        product_ids = products.filtered(lambda product: product.type in ('product', 'consu')).ids

        boms = self.env['mrp.bom'].browse(self.env.registry.populated_models['mrp.bom'])
        boms_by_company = defaultdict(OrderedSet)
        for bom in boms:
            boms_by_company[bom.company_id.id].add(bom.id)
        boms_by_company = {company_id: self.env['mrp.bom'].browse(boms) for company_id, boms in boms_by_company.items()}
        boms_by_company = {
            company_id: boms | boms_by_company.get(False, self.env['mrp.bom'])
            for company_id, boms in boms_by_company.items()}

        def get_bom_id(values, counter, random):
            if random.random() > 0.7:  # 30 % of prototyping
                return False
            return random.choice(boms_by_company[values['company_id']]).id

        def get_consumption(values, counter, random):
            if not values['bom_id']:
                return 'flexible'
            return self.env['mrp.bom'].browse(values['bom_id']).consumption

        def get_product_id(values, counter, random):
            if not values['bom_id']:
                return random.choice(product_ids)
            bom = self.env['mrp.bom'].browse(values['bom_id'])
            return bom.product_id.id or random.choice(bom.product_tmpl_id.product_variant_ids.ids)

        def get_product_uom_id(values, counter, random):
            product = self.env['product.product'].browse(values['product_id'])
            return product.uom_id.id

        # Fetch all stock picking type and group then by company_id
        manu_picking_types = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')])
        manu_picking_types_by_company_id = defaultdict(OrderedSet)
        for picking_type in manu_picking_types:
            manu_picking_types_by_company_id[picking_type.company_id.id].add(picking_type.id)
        manu_picking_types_by_company_id = {company_id: list(picking_ids) for company_id, picking_ids in manu_picking_types_by_company_id.items()}

        def get_picking_type_id(values, counter, random):
            return random.choice(manu_picking_types_by_company_id[values['company_id']])

        def get_location_src_id(values, counter, random):
            # TODO : add some randomness
            picking_type = self.env['stock.picking.type'].browse(values['picking_type_id'])
            return picking_type.default_location_src_id.id

        def get_location_dest_id(values, counter, random):
            # TODO : add some randomness
            picking_type = self.env['stock.picking.type'].browse(values['picking_type_id'])
            return picking_type.default_location_dest_id.id

        def get_date_start(values, counter, random):
            # 95.45 % of picking scheduled between (-10, 30) days and follow a gauss distribution (only +-15% picking is late)
            delta = random.gauss(10, 10)
            return now + timedelta(days=delta)

        return [
            ('company_id', populate.iterate(company_ids)),
            ('bom_id', populate.compute(get_bom_id)),
            ('consumption', populate.compute(get_consumption)),
            ('product_id', populate.compute(get_product_id)),
            ('product_uom_id', populate.compute(get_product_uom_id)),
            ('product_qty', populate.randint(1, 10)),
            ('picking_type_id', populate.compute(get_picking_type_id)),
            ('date_start', populate.compute(get_date_start)),
            ('location_src_id', populate.compute(get_location_src_id)),
            ('location_dest_id', populate.compute(get_location_dest_id)),
            ('priority', populate.iterate(['0', '1'], [0.95, 0.05])),
        ]


class StockMove(models.Model):
    _inherit = 'stock.move'

    _populate_dependencies = ['stock.picking', 'mrp.production']

    def _populate(self, size):
        moves = super()._populate(size)

        def confirm_prototype_mo(sample_ratio):
            # Confirm X % of prototype MO
            random = populate.Random('confirm_prototype_mo')
            mo_ids = moves.raw_material_production_id.ids
            mo_to_confirm = self.env['mrp.production'].browse(random.sample(mo_ids, int(len(mo_ids) * 0.8)))
            _logger.info("Confirm %d of prototype MO" % len(mo_to_confirm))
            mo_to_confirm.action_confirm()

        # (Un)comment this line to confirm a part of prototype MO, can be useful to check performance
        # confirm_prototype_mo(0.8)

        return moves.exists()  # Confirm Mo can unlink moves

    def _populate_attach_record_weight(self):
        fields, weight = super()._populate_attach_record_weight()
        return fields + ['raw_material_production_id'], weight + [1]

    def _populate_attach_record_generator(self):

        productions = self.env['mrp.production'].browse(self.env.registry.populated_models['mrp.production'])
        productions = productions.filtered(lambda prod: not prod.bom_id)

        def next_production_id():
            while productions:
                yield from productions.ids

        return {**super()._populate_attach_record_generator(), 'raw_material_production_id': next_production_id()}

    def _populate_factories(self):

        def _compute_production_values(iterator, field_name, model_name):
            for values in iterator:

                if values.get('raw_material_production_id'):
                    production = self.env['mrp.production'].browse(values['raw_material_production_id'])
                    values['location_id'] = production.location_src_id.id
                    values['location_dest_id'] = production.production_location_id.id
                    values['picking_type_id'] = production.picking_type_id.id
                    values['name'] = production.name
                    values['date'] = production.date_start
                    values['company_id'] = production.company_id.id
                yield values

        return super()._populate_factories() + [
            ('_compute_production_values', _compute_production_values),
        ]
