import logging
from pathlib import Path
from typing import Union

from namedlist import namedlist

from ee.doit import DoItConfig
from ee.ds import DataSet, create_message

logger = logging.getLogger(__name__)

doit_config = DoItConfig()


class BomComponent(object):
    def __init__(self, ref, mpn):
        self.ref = ref
        self.mpn = mpn

    def to_object(self, ds):
        return ds.create_object("bom-component", self.ref). \
            set("ref", self.ref). \
            set("mpn", self.mpn)


def task_bom():
    """
    Creates 'bom-component' from 'component'.

    Takes all schematic components, filters out all virtual/non-
    physical components (like power flags and ground components) and
    creates 'bom-component' objects.
    """

    out_data_set, in_data_sets = doit_config.data_sets_for(task_bom)

    def action():
        in_ds = doit_config.dsm.load_data_sets(in_data_sets)

        with doit_config.dsm.create_rw(out_data_set, clean=True) as output:
            components = [o for o in in_ds.items() if o.object_type.name == "component"]

            bom_components = {}

            for c in components:
                ref = c.get("ref")
                mpn = c.get("mpn")

                if not ref:
                    raise Exception("Missing ref")

                if not mpn:
                    create_message(output, "Missing required field 'mpn' on component ref={}".format(ref), "error")
                    continue

                if ref in bom_components:
                    raise Exception("Duplicate ref '{}'".format("ref"))

                bom_components[ref] = BomComponent(ref, mpn)

            [c.to_object(output) for c in bom_components.values()]

    return {
        "file_dep": [doit_config.dsm.cookie_for_ds(ds) for ds in in_data_sets],
        "actions": [action],
        "targets": [doit_config.dsm.cookie_for_ds(out_data_set)],
    }


doit_config.set_data_sets_for(task_bom, "bom", "components")


def order_csv(count: int, style: str, output_file: Path, out_ds: DataSet, data_sets):
    ds = doit_config.dsm.load_data_sets(data_sets)

    csv_ds = DataSet()

    parts = {}

    # noinspection PyPep8Naming
    Part = namedlist("Part", "mpn, cnt, refs, digikey_pn")

    digikey_parts = [o for o in ds.items() if
                     o.object_type.name == "component-to-part-mapping" and
                     o.get("seller") == "digikey"]

    for c in [o for o in ds.items() if o.object_type.name == "bom-component"]:
        ref = c.get("ref")
        mpn = c.get("mpn")

        digikey_pn = None

        if style == "digikey":

            digikey_pns = []
            for o in (o for o in digikey_parts if o.get("ref") == ref):

                t = o.values("part-number", strip=True, required=True)

                if not t:
                    create_message(out_ds, "Missing required field value for field part-number, object key={}".
                                   format(o.key), level="error")

                part_number = t[0]

                digikey_pns.append(part_number)

            # The actual DK part should depend on the price breaks for the different packagings, but we don't have that
            # info here. Luckily the DK store proposes cheaper packagings when ordering so that is something.

            digikey_pns = set(digikey_pns)

            if len(digikey_pns) == 0:
                create_message(out_ds, "No part for component: ref={}, mpn={}".format(ref, mpn), "error")
                continue
            elif len(digikey_pns) > 1:
                create_message(out_ds, "Multiple parts for component: ref={}, mpn={}. Don't know which one to select.".
                               format(ref, mpn), "error")
                continue
            else:
                digikey_pn = next(iter(digikey_pns))

            digikey_pn = digikey_pn.strip()

            if len(digikey_pn) == 0:
                raise Exception("Missing digikey part number for ref={}".format(ref))

        if mpn in parts:
            part = parts[mpn]

            if digikey_pn:
                if part.digikey_pn != digikey_pn:
                    raise Exception("Bad data, inconsistent digikey-pn for mpn '{}'. First digikey part number='{}', "
                                    "new digikey part number='{}'".format(mpn, part.digikey_pn, digikey_pn))

            part.cnt += 1
            part.refs.append(ref)
        else:
            parts[mpn] = Part(mpn=mpn, cnt=1, refs=[ref], digikey_pn=digikey_pn)

    mpn_field = "MPN"
    count_field = "Count"
    refs_field = "References"

    if style == "digikey":
        count_field = "Quantity"
        refs_field = "Customer Reference"

    for part in sorted(parts.values(), key=lambda p: p.mpn):
        o = csv_ds.create_object("row", part.mpn). \
            set(mpn_field, part.mpn). \
            set(count_field, part.cnt * count). \
            set(refs_field, ",".join(part.refs))

        if style == "digikey":
            o.set("Digi-Key Part Number", part.digikey_pn)

    fields = None
    include_extra_fields = True

    if style == "digikey":
        fields = ["Digi-Key Part Number", refs_field, count_field, mpn_field]
        include_extra_fields = False

    doit_config.dsm.store_csv(output_file, csv_ds, "row", order_by=mpn_field, fields=fields,
                              include_extra_fields=include_extra_fields)


def create_task_order_csv(*, style: str = None, output_file: Union[str, Path], out_data_set, data_sets, count: int = 1):
    def action():
        with doit_config.dsm.create_rw(out_data_set, clean=True) as out:
            order_csv(count, style, Path(output_file), out, data_sets)

    return {
        "name": "order-{}".format(count) if not style else "order-{}-{}".format(style, count),
        "actions": [action],
        "file_dep": [doit_config.dsm.cookie_for_ds(ds) for ds in data_sets],
        "targets": [doit_config.dsm.cookie_for_ds(out_data_set), output_file],
    }