"""Flow that builds a Harness model from YAML inputs and renders outputs."""

import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union

from filare.models.metadata import Metadata
from filare.models.notes import Notes, get_page_notes
from filare.models.options import PageOptions, get_page_options
from filare.models.connector import ConnectorModel
from filare.models.cable import CableModel
from filare.models.component import ComponentModel
from filare.parser import parse_concat_merge_files
from filare.models.dataclasses import AUTOGENERATED_PREFIX
from filare.models.harness import Harness
from filare.models.utils import expand, get_single_key_and_value, smart_file_resolve

from .render_outputs import render_harness_outputs


def _build_metadata(yaml_file, yaml_data, extra_metadata, metadata_output_name):
    return Metadata(
        **{
            **{
                "output_name": metadata_output_name,
                "title": yaml_file.stem,
            },
            **yaml_data.get("metadata", {}),
            **extra_metadata,
        }
    )


def _collect_templates(yaml_data, image_paths):
    template_connectors = {}
    template_cables = {}
    connection_sets = []
    sections = ["connectors", "cables", "connections"]
    types = [dict, dict, list]
    for sec, ty in zip(sections, types):
        if sec in yaml_data and type(yaml_data[sec]) == ty:
            if len(yaml_data[sec]) > 0:
                if ty == dict:
                    for key, attribs in yaml_data[sec].items():
                        image = attribs.get("image")
                        if isinstance(image, dict):
                            image_path = Path(image["src"])
                            if image_path and not image_path.is_absolute():
                                attribs["image"]["src"] = smart_file_resolve(
                                    image_path, image_paths
                                )
                        if sec == "connectors":
                            template_connectors[key] = attribs
                        elif sec == "cables":
                            template_cables[key] = attribs
            else:
                pass
        else:
            if ty == dict:
                yaml_data[sec] = {}
            elif ty == list:
                yaml_data[sec] = []
    connection_sets = yaml_data["connections"]
    return template_connectors, template_cables, connection_sets


def build_harness_from_models(
    connector_models: List[Union[ConnectorModel, Dict[str, Any]]],
    cable_models: List[Union[CableModel, Dict[str, Any]]],
    metadata: Metadata,
    options: PageOptions,
    notes: Notes = Notes([]),
    additional_bom: Optional[List[Union[ComponentModel, Dict[str, Any]]]] = None,
) -> Harness:
    """Build a Harness from already-instantiated connector/cable models."""
    harness = Harness(metadata=metadata, options=options, notes=notes)
    for connector in connector_models:
        harness.add_connector_model(connector)
    for cable in cable_models:
        harness.add_cable_model(cable)
    for item in additional_bom or []:
        harness.add_additional_bom_item(item)
    harness.populate_bom()
    return harness


def _normalize_connection_set(
    connection_set,
    template_separator_char,
    designators_and_templates,
    autogenerated_designators,
):
    def resolve_designator(inp, separator):
        if separator in inp:
            if inp.count(separator) > 1:
                raise Exception(f"{inp} - Found more than one separator ({separator})")
            template, designator = inp.split(separator)
            if designator == "":
                autogenerated_designators[template] = (
                    autogenerated_designators.get(template, 0) + 1
                )
                designator = (
                    f"{AUTOGENERATED_PREFIX}"
                    f"{template}_{autogenerated_designators[template]}"
                )
            if designator in designators_and_templates:
                if designators_and_templates[designator] != template:
                    raise Exception(
                        f"Trying to redefine {designator}"
                        f" from {designators_and_templates[designator]} to {template}"
                    )
            else:
                designators_and_templates[designator] = template
        else:
            template, designator = (inp, inp)
            if designator in designators_and_templates:
                pass
            else:
                designators_and_templates[designator] = template
        return (template, designator)

    connectioncount = []
    for entry in connection_set:
        if isinstance(entry, list):
            connectioncount.append(len(entry))
        elif isinstance(entry, dict):
            connectioncount.append(len(expand(list(entry.values())[0])))

    if not any(connectioncount):
        raise ValueError("No connection count found in connection set")

    if len(set(connectioncount)) > 1:
        pretty_sets = [
            f'length {len(v)}: "{k}": {v}' for s in connection_set for k, v in s.items()
        ]
        prettyer_sets = "\n\t" + "\n\t".join(pretty_sets)
        raise Exception(
            f"All items in connection set must reference the same number of connections\nIt is not the case for:{prettyer_sets}"
        )

    connectioncount = connectioncount[0]

    for index, entry in enumerate(connection_set):
        if isinstance(entry, str):
            connection_set[index] = [entry] * connectioncount

    for index, entry in enumerate(connection_set):
        if isinstance(entry, list):
            for subindex, item in enumerate(entry):
                template, designator = resolve_designator(item, template_separator_char)
                connection_set[index][subindex] = designator
        elif isinstance(entry, dict):
            key = list(entry.keys())[0]
            template, designator = resolve_designator(key, template_separator_char)
            value = entry[key]
            connection_set[index] = {designator: value}

    for index, entry in enumerate(connection_set):
        if isinstance(entry, list):
            connection_set[index] = [{designator: 1} for designator in entry]
        elif isinstance(entry, dict):
            designator = list(entry.keys())[0]
            pinlist = expand(entry[designator])
            connection_set[index] = [{designator: pin} for pin in pinlist]

    return connection_set, connectioncount


def build_harness_from_files(
    inp: List[Path],
    metadata_files: List[Path],
    return_types: Union[None, str, Tuple[str]] = None,
    output_formats: Union[None, str, Tuple[str]] = None,
    output_dir: Path = None,
    extra_metadata: Dict = {},
    shared_bom: Dict = {},
    output_name_override: str = None,
    connector_view: str = "detailed",
    metadata_output_name: str = None,
    update_shared_bom: bool = True,
) -> Any:
    if not output_formats and not return_types:
        raise Exception("No output formats or return types specified")

    yaml_file = inp[-1]
    yaml_data = parse_concat_merge_files(inp, metadata_files)

    image_paths = {f.parent for f in inp if f.parent.is_dir()}

    output_dir = yaml_file.parent if not output_dir else output_dir
    output_name = output_name_override if output_name_override else yaml_file.stem
    metadata_output_name = metadata_output_name or output_name

    try:
        metadata = _build_metadata(
            yaml_file, yaml_data, extra_metadata, metadata_output_name
        )
    except TypeError:
        logging.error(
            "Metadata definition is missing an argument, refer to trace for which one\n\tsee src/filaree/metdata.py for a definition of the metadata fields"
        )
        raise

    harness = Harness(
        metadata=metadata,
        options=get_page_options(yaml_data, output_name),
        notes=get_page_notes(yaml_data, output_name),
        shared_bom=shared_bom,
    )
    designators_and_templates = {}
    autogenerated_designators = {}

    template_connectors, template_cables, connection_sets = _collect_templates(
        yaml_data, image_paths
    )

    alternating_types = ["connector", "cable"]
    expected_type = None

    def check_type(designator, template, actual_type):
        nonlocal expected_type
        if not expected_type:
            expected_type = actual_type

        if actual_type != expected_type:
            raise Exception(
                f'Expected {expected_type}, but "{designator}" ("{template}") is {actual_type}'
            )

    def alternate_type():
        nonlocal expected_type
        expected_type = alternating_types[1 - alternating_types.index(expected_type)]

    template_separator_char = harness.options.template_separator

    for connection_set in connection_sets:
        connection_set, connectioncount = _normalize_connection_set(
            connection_set,
            template_separator_char,
            designators_and_templates,
            autogenerated_designators,
        )

        expected_type = None

        for entry in connection_set:
            for item in entry:
                designator = list(item.keys())[0]
                template = designators_and_templates[designator]

                if designator in harness.connectors:
                    check_type(designator, template, "connector")
                elif template in template_connectors.keys():
                    check_type(designator, template, "connector")
                    harness.add_connector(
                        designator=designator, **template_connectors[template]
                    )

                elif designator in harness.cables:
                    check_type(designator, template, "cable")
                elif template in template_cables.keys():
                    check_type(designator, template, "cable")
                    harness.add_cable(
                        designator=designator, **template_cables[template]
                    )
                else:
                    raise Exception(f"{template} is an unknown template/designator")

            alternate_type()

        connection_set = list(map(list, zip(*connection_set)))

        for index_entry, entry in enumerate(connection_set):
            for index_item, item in enumerate(entry):
                designator = list(item.keys())[0]

                if designator in harness.cables:
                    if index_item == 0:
                        from_name, from_pin = (None, None)
                    else:
                        from_name, from_pin = get_single_key_and_value(
                            entry[index_item - 1]
                        )
                    via_name, via_pin = (designator, item[designator])
                    if index_item == len(entry) - 1:
                        to_name, to_pin = (None, None)
                    else:
                        to_name, to_pin = get_single_key_and_value(
                            entry[index_item + 1]
                        )
                    harness.connect(
                        from_name, from_pin, via_name, via_pin, to_name, to_pin
                    )

    if "additional_bom_items" in yaml_data:
        for line in yaml_data["additional_bom_items"]:
            try:
                harness.add_additional_bom_item(line)
            except TypeError as e:
                logging.error(f"Failed to add line {line} as an additional bom item")
                raise

    if connector_view == "overview":
        harness.orient_connectors_overview()

    if connector_view == "overview":
        for connector in harness.connectors.values():
            if connector.style != "simple":
                connector.style = "overview"
        for cable in harness.cables.values():
            cable.style = "overview"

    harness.populate_bom()

    if output_formats:
        render_harness_outputs(harness, output_dir, output_name, output_formats)

    if return_types:
        if isinstance(return_types, str):
            return_types = [return_types]

        return_types = [t.lower() for t in return_types]

        returns = {}
        for rt in return_types:
            if rt == "png":
                returns["png"] = harness.png
            if rt == "svg":
                returns["svg"] = harness.svg
            if rt == "harness":
                returns["harness"] = harness
            if rt == "shared_bom":
                returns["shared_bom"] = harness.shared_bom

        return returns
