"""GOST table generators (ported from desktop table controllers)."""

from __future__ import annotations

from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Any, Optional

from app.services.designators import (
    designator_sort_key,
    format_consecutive_ranges,
    format_designator_range,
    get_type_names,
    resolve_expression,
    split_designator,
    split_long_text,
)


@dataclass
class ComponentView:
    designator: str
    properties: dict[str, str] = field(default_factory=dict)
    is_fitted: bool = True

    def get(self, key: str, default: str = "") -> str:
        raw = self.properties.get(key, default)
        return resolve_expression(raw, self.properties) if raw else default


@dataclass
class MaterialView:
    name: str
    value: str
    height: float
    diel_type: int
    layer_number: int


def _empty_flags(**extra: Any) -> dict[str, Any]:
    base = {
        "is_header": False,
        "is_empty": False,
        "is_auto_generated": True,
        "stretch": False,
        "is_underline": False,
        "page_number": 1,
    }
    base.update(extra)
    return base


def _paginate(rows: list[dict[str, Any]], first: int, other: int) -> list[dict[str, Any]]:
    page = 1
    used = 0
    limit = first
    for i, row in enumerate(rows):
        if used >= limit:
            page += 1
            used = 0
            limit = other
        row["row_index"] = i
        row["page_number"] = page
        used += 1
    return rows


def _group_by_letter(components: list[ComponentView]) -> OrderedDict[str, list[ComponentView]]:
    groups: OrderedDict[str, list[ComponentView]] = OrderedDict()
    for comp in sorted(components, key=lambda c: designator_sort_key(c.designator)):
        letter, _ = split_designator(comp.designator)
        letter = letter.upper()
        groups.setdefault(letter, []).append(comp)
    return groups


def generate_perechen(
    components: list[ComponentView],
    name_field: str = "Name",
    mappings: dict[str, tuple[str, str]] | None = None,
) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    fitted = [c for c in components if c.is_fitted]
    groups = _group_by_letter(fitted)

    for letter, comps in groups.items():
        singular, plural = get_type_names(comps[0].designator, mappings)
        if len(comps) > 1:
            rows.append(_empty_flags(is_empty=True, position="", designation="", quantity="", note=""))
            rows.append(
                _empty_flags(
                    is_header=True,
                    position="",
                    designation=plural,
                    quantity="",
                    note="",
                    is_underline=True,
                )
            )
            rows.append(_empty_flags(is_empty=True, position="", designation="", quantity="", note=""))

            # subgroup by name value, preserving order
            subgroups: OrderedDict[str, list[ComponentView]] = OrderedDict()
            for c in comps:
                key = c.get(name_field, "")
                subgroups.setdefault(key, []).append(c)

            for value, group in subgroups.items():
                desigs = [c.designator for c in group]
                desig_text = format_designator_range(desigs)
                desig_parts = split_long_text(desig_text, 60)
                name_parts = split_long_text(value, 60)
                max_rows = max(len(desig_parts), len(name_parts), 1)
                for i in range(max_rows):
                    rows.append(
                        _empty_flags(
                            position=desig_parts[i] if i < len(desig_parts) else "",
                            designation=name_parts[i] if i < len(name_parts) else "",
                            quantity=str(len(group)) if i == 0 else "",
                            note="",
                        )
                    )
        else:
            c = comps[0]
            value = c.get(name_field, "")
            designation = f"{singular} {value}".strip()
            rows.append(_empty_flags(is_empty=True, position="", designation="", quantity="", note=""))
            rows.append(
                _empty_flags(
                    position=c.designator,
                    designation=designation,
                    quantity="1",
                    note="",
                )
            )

    return _paginate(rows, 25, 32)


def generate_specification_pcb(
    components: list[ComponentView],
    decimal_number: str = "",
    board_name: str = "",
    pcb_doc_name: str = "",
    mappings: dict[str, tuple[str, str]] | None = None,
) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    dec = decimal_number or ""

    def add_empty():
        rows.append(
            _empty_flags(
                format="", zone="", position="", designation="", name="", quantity="", note="", is_empty=True
            )
        )

    def add_header(name: str):
        rows.append(
            _empty_flags(
                format="",
                zone="",
                position="",
                designation="",
                name=name,
                quantity="",
                note="",
                is_header=True,
                is_underline=True,
            )
        )

    def add_doc(fmt: str, designation: str, name: str, note: str = "", pos: str = ""):
        rows.append(
            _empty_flags(
                format=fmt,
                zone="",
                position=pos,
                designation=designation,
                name=name,
                quantity="",
                note=note,
            )
        )

    add_empty()
    add_header("Документация")
    add_empty()
    add_doc("A1", f"{dec} СБ", "Сборочный чертеж")
    add_doc("A3", f"{dec} Э3", "Схема электрическая принципиальная")
    add_doc("A4", f"{dec} ПЭ3", "Перечень элементов")
    add_doc("A3", f"{dec} ВП", "Ведомость покупных изделий")
    add_doc("*)", f"{dec} Д33", "Данные результатов проектирования", "DVD диск")
    add_empty()
    add_doc("А4", f"{dec} Д10-УЛ", "Удостоверяющий лист", "Размножать")
    add_doc("", "", "Данные результатов проектирования", "по указанию")
    add_empty()
    add_header("Сборочные единицы")
    add_empty()
    rows.append(
        _empty_flags(
            format="",
            zone="",
            position="1",
            designation=f"{dec} платы" if dec else "платы",
            name=board_name,
            quantity="",
            note="",
        )
    )
    add_empty()
    add_header("Детали")
    add_empty()
    add_header("Стандартные изделия")
    add_empty()
    add_header("Прочие изделия")
    add_empty()

    pos = 2
    fitted = [c for c in components if c.is_fitted]
    groups = _group_by_letter(fitted)

    for letter, comps in groups.items():
        subgroups: OrderedDict[str, list[ComponentView]] = OrderedDict()
        for c in comps:
            key = c.get("ManufacturerPartNumber", "") or c.get("Name", "")
            subgroups.setdefault(key, []).append(c)

        for part_number, group in subgroups.items():
            singular, plural = get_type_names(group[0].designator, mappings)
            qty = len(group)
            type_name = plural if qty > 1 else singular
            desigs = format_consecutive_ranges([c.designator for c in group])
            note_parts = split_long_text(desigs, 11)
            name_full = f"{type_name} {part_number}".strip()
            name_parts = split_long_text(name_full, 34)
            # if type + first part too long, put type on its own line
            if name_parts and len(f"{type_name} {name_parts[0]}") > 34 and part_number:
                name_parts = [type_name] + split_long_text(part_number, 34)

            max_rows = max(len(name_parts), len(note_parts), 1)
            for i in range(max_rows):
                rows.append(
                    _empty_flags(
                        format="",
                        zone="",
                        position=str(pos) if i == 0 else "",
                        designation="",
                        name=name_parts[i] if i < len(name_parts) else "",
                        quantity=str(qty) if i == 0 else "",
                        note=note_parts[i] if i < len(note_parts) else "",
                    )
                )
            pos += 1

    add_empty()
    add_header("Примечание")
    rows.append(
        _empty_flags(
            format="", zone="", position="", designation="", name="Изготовить плату печатную", quantity="", note=""
        )
    )
    pcb_short = Path_name(pcb_doc_name)
    rows.append(
        _empty_flags(
            format="",
            zone="",
            position="",
            designation="",
            name=f"По файлу {pcb_short}" if pcb_short else "По файлу",
            quantity="",
            note="",
        )
    )
    rows.append(
        _empty_flags(
            format="",
            zone="",
            position="",
            designation="",
            name=f"из состава {dec} Д10" if dec else "из состава Д10",
            quantity="",
            note="",
        )
    )

    return _paginate(rows, 26, 32)


def Path_name(path: str) -> str:
    if not path:
        return ""
    return path.replace("\\", "/").split("/")[-1]


def generate_specification(
    materials: list[MaterialView],
    layer_count: int = 0,
    decimal_number: str = "",
) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    dec = decimal_number or ""

    def add_empty():
        rows.append(
            _empty_flags(
                format="", zone="", position="", designation="", name="", quantity="", note="", is_empty=True
            )
        )

    def add_header(name: str):
        rows.append(
            _empty_flags(
                format="",
                zone="",
                position="",
                designation="",
                name=name,
                quantity="",
                note="",
                is_header=True,
                is_underline=True,
            )
        )

    add_empty()
    add_header("Документация")
    add_empty()
    rows.append(
        _empty_flags(
            format="A1",
            zone="",
            position="",
            designation=f"{dec}Э3 СБ",
            name="Сборочный чертеж",
            quantity="",
            note="",
        )
    )
    rows.append(
        _empty_flags(
            format="*)",
            zone="",
            position="",
            designation=f"{dec} Э3 Т5М",
            name="Данные проектирования",
            quantity="",
            note="",
        )
    )
    add_empty()
    add_header("Материалы")
    add_empty()

    sorted_mats = sorted(materials, key=lambda m: m.layer_number)
    by_name: OrderedDict[str, list[MaterialView]] = OrderedDict()
    for m in sorted_mats:
        by_name.setdefault(m.name, []).append(m)

    core_count = sum(1 for m in materials if m.diel_type == 1)
    foil_count = max(layer_count - core_count * 2, 0)
    pos = 1

    for name, group in by_name.items():
        if name == "DielMaterial":
            by_value: OrderedDict[str, list[MaterialView]] = OrderedDict()
            for m in group:
                by_value.setdefault(m.value, []).append(m)
            for value, subgroup in by_value.items():
                display = {
                    "FR4 PR": "Препрег FR4 PR",
                    "FR4 Tg150": "Стеклотекстолит FR4 Tg150",
                    "Solder Resist": "Паяльная маска",
                }.get(value, value)
                thickness = subgroup[0].height
                display = f"{display} {thickness:.3f} мм"
                rows.append(
                    _empty_flags(
                        format="",
                        zone="",
                        position=str(pos),
                        designation="",
                        name=display,
                        quantity=str(len(subgroup)),
                        note="",
                    )
                )
                pos += 1
        else:
            rows.append(
                _empty_flags(
                    format="",
                    zone="",
                    position=str(pos),
                    designation="",
                    name=name,
                    quantity=str(len(group)),
                    note="",
                )
            )
            pos += 1

    if foil_count > 0:
        rows.append(
            _empty_flags(
                format="",
                zone="",
                position=str(pos),
                designation="",
                name="Фольга медная толщиной 18 мкм",
                quantity=str(foil_count),
                note="",
            )
        )

    return _paginate(rows, 27, 33)


def generate_vedomost(
    components: list[ComponentView],
    where_used: str = "",
    mappings: dict[str, tuple[str, str]] | None = None,
    column_mappings: Optional[dict[str, str]] = None,
) -> list[dict[str, Any]]:
    cmap = column_mappings or {
        "Name": "Name",
        "ProductCode": "ProductCode",
        "DocumentCode": "DocumentCode",
        "Supplier": "Supplier",
        "Note": "Note",
    }
    rows: list[dict[str, Any]] = []
    fitted = [c for c in components if c.is_fitted]
    groups = _group_by_letter(fitted)

    for letter, comps in groups.items():
        subgroups: OrderedDict[str, list[ComponentView]] = OrderedDict()
        for c in comps:
            key = c.get(cmap.get("Name", "Name"), "")
            subgroups.setdefault(key, []).append(c)

        multiple_names = len(subgroups) > 1
        if len(comps) > 1 and multiple_names:
            _, plural = get_type_names(comps[0].designator, mappings)
            rows.append(
                _empty_flags(
                    is_empty=True,
                    name="",
                    product_code="",
                    document_code="",
                    supplier="",
                    where_used="",
                    quantity_per_item="",
                    quantity_in_set="",
                    quantity_for_reg="",
                    total_quantity="",
                    note="",
                )
            )
            rows.append(
                _empty_flags(
                    is_header=True,
                    is_underline=True,
                    name=plural,
                    product_code="",
                    document_code="",
                    supplier="",
                    where_used="",
                    quantity_per_item="",
                    quantity_in_set="",
                    quantity_for_reg="",
                    total_quantity="",
                    note="",
                )
            )
            rows.append(
                _empty_flags(
                    is_empty=True,
                    name="",
                    product_code="",
                    document_code="",
                    supplier="",
                    where_used="",
                    quantity_per_item="",
                    quantity_in_set="",
                    quantity_for_reg="",
                    total_quantity="",
                    note="",
                )
            )

        for value, group in subgroups.items():
            singular, plural = get_type_names(group[0].designator, mappings)
            qty = len(group)
            type_name = ""
            if len(comps) == 1 or (len(comps) > 1 and not multiple_names):
                type_name = plural if qty > 1 else singular

            name = f"{type_name} {value}".strip() if type_name else value
            product = group[0].get(cmap.get("ProductCode", "ProductCode"), "")
            document = group[0].get(cmap.get("DocumentCode", "DocumentCode"), "")
            supplier = group[0].get(cmap.get("Supplier", "Supplier"), "")
            note = group[0].get(cmap.get("Note", "Note"), "")

            name_parts = split_long_text(name, 32)
            product_parts = split_long_text(product, 25)
            document_parts = split_long_text(document, 35)
            supplier_parts = split_long_text(supplier, 25)
            where_parts = split_long_text(where_used, 35)
            note_parts = split_long_text(note, 16)
            max_rows = max(
                len(name_parts),
                len(product_parts),
                len(document_parts),
                len(supplier_parts),
                len(where_parts),
                len(note_parts),
                1,
            )

            if len(comps) == 1 or (len(comps) > 1 and not multiple_names):
                rows.append(
                    _empty_flags(
                        is_empty=True,
                        name="",
                        product_code="",
                        document_code="",
                        supplier="",
                        where_used="",
                        quantity_per_item="",
                        quantity_in_set="",
                        quantity_for_reg="",
                        total_quantity="",
                        note="",
                    )
                )

            for i in range(max_rows):
                rows.append(
                    _empty_flags(
                        name=name_parts[i] if i < len(name_parts) else "",
                        product_code=product_parts[i] if i < len(product_parts) else "",
                        document_code=document_parts[i] if i < len(document_parts) else "",
                        supplier=supplier_parts[i] if i < len(supplier_parts) else "",
                        where_used=where_parts[i] if i < len(where_parts) else "",
                        quantity_per_item=str(qty) if i == 0 else "",
                        quantity_in_set="",
                        quantity_for_reg="",
                        total_quantity=str(qty) if i == 0 else "",
                        note=note_parts[i] if i < len(note_parts) else "",
                    )
                )

            if len(comps) == 1 or (len(comps) > 1 and not multiple_names):
                rows.append(
                    _empty_flags(
                        is_empty=True,
                        name="",
                        product_code="",
                        document_code="",
                        supplier="",
                        where_used="",
                        quantity_per_item="",
                        quantity_in_set="",
                        quantity_for_reg="",
                        total_quantity="",
                        note="",
                    )
                )

    return _paginate(rows, 24, 29)


def generate_simple_list(
    components: list[ComponentView],
    name_field: str = "Name",
    tech_reserve_percent: float = 10.0,
    boards_count: int = 1,
) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    fitted = sorted(
        [c for c in components if c.is_fitted],
        key=lambda c: designator_sort_key(c.designator),
    )
    # group consecutive same name
    groups: list[list[ComponentView]] = []
    current: list[ComponentView] = []
    current_name = None
    for c in fitted:
        name = c.get(name_field, "")
        if current and name != current_name:
            groups.append(current)
            current = []
        current.append(c)
        current_name = name
    if current:
        groups.append(current)

    for group in groups:
        base = len(group)
        reserve = max(1, round(base * boards_count * tech_reserve_percent / 100))
        qty = boards_count * base + reserve
        desigs = format_designator_range([c.designator for c in group])
        rows.append(
            {
                "row_index": 0,
                "designator": desigs,
                "name": group[0].get(name_field, ""),
                "quantity": str(qty),
                "is_auto_generated": True,
                "is_empty": False,
            }
        )

    for i, row in enumerate(rows):
        row["row_index"] = i
    return rows


TABLE_TYPES = ("perechen", "specification_pcb", "specification", "vedomost", "simple_list")
