from __future__ import annotations

from typing import Any

from sqlalchemy import delete, select
from sqlalchemy.orm import Session, selectinload

from app.models import (
    Component,
    ComponentVariant,
    DesignatorMapping,
    PerechenRow,
    Project,
    SimpleListRow,
    SpecificationPcbRow,
    SpecificationRow,
    TitleInscription,
    Variant,
    VariantProperty,
    VedomostRow,
)
from app.services.table_generators import (
    TABLE_TYPES,
    ComponentView,
    MaterialView,
    generate_perechen,
    generate_simple_list,
    generate_specification,
    generate_specification_pcb,
    generate_vedomost,
)

ROW_MODELS = {
    "perechen": PerechenRow,
    "specification_pcb": SpecificationPcbRow,
    "specification": SpecificationRow,
    "vedomost": VedomostRow,
    "simple_list": SimpleListRow,
}

EDITABLE_FIELDS = {
    "perechen": {"position", "designation", "quantity", "note", "is_header", "is_empty", "stretch", "is_underline"},
    "specification_pcb": {
        "format", "zone", "position", "designation", "name", "quantity", "note",
        "is_header", "is_empty", "stretch", "is_underline",
    },
    "specification": {
        "format", "zone", "position", "designation", "name", "quantity", "note",
        "is_header", "is_empty", "stretch", "is_underline",
    },
    "vedomost": {
        "name", "product_code", "document_code", "supplier", "where_used",
        "quantity_per_item", "quantity_in_set", "quantity_for_reg", "total_quantity", "note",
        "is_header", "is_empty", "stretch", "is_underline",
    },
    "simple_list": {"designator", "name", "quantity", "is_empty"},
}


def _mappings_dict(db: Session, project_id: int) -> dict[str, tuple[str, str]]:
    rows = db.scalars(
        select(DesignatorMapping).where(DesignatorMapping.project_id == project_id)
    ).all()
    return {r.prefix: (r.singular_name, r.plural_name) for r in rows}


def load_components_for_variant(db: Session, project: Project) -> list[ComponentView]:
    variant = db.scalar(
        select(Variant).where(
            Variant.project_id == project.id,
            Variant.name == project.current_variant,
        )
    )
    components = db.scalars(
        select(Component)
        .where(Component.project_id == project.id)
        .options(selectinload(Component.properties))
    ).all()

    views: list[ComponentView] = []
    for comp in components:
        props = {p.key: p.value for p in comp.properties}
        is_fitted = True
        if variant:
            link = db.scalar(
                select(ComponentVariant).where(
                    ComponentVariant.component_id == comp.id,
                    ComponentVariant.variant_id == variant.id,
                )
            )
            if link is not None:
                is_fitted = link.is_fitted
            overrides = db.scalars(
                select(VariantProperty).where(
                    VariantProperty.component_id == comp.id,
                    VariantProperty.variant_id == variant.id,
                )
            ).all()
            for o in overrides:
                props[o.key] = o.value
        views.append(ComponentView(designator=comp.designator, properties=props, is_fitted=is_fitted))
    return views


def row_to_dict(row: Any) -> dict[str, Any]:
    data = {}
    for col in row.__table__.columns:
        data[col.name] = getattr(row, col.name)
    return data


def get_rows(db: Session, project_id: int, table_type: str) -> list[dict[str, Any]]:
    model = ROW_MODELS[table_type]
    rows = db.scalars(
        select(model).where(model.project_id == project_id).order_by(model.row_index)
    ).all()
    return [row_to_dict(r) for r in rows]


def replace_rows(db: Session, project_id: int, table_type: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    model = ROW_MODELS[table_type]
    db.execute(delete(model).where(model.project_id == project_id))
    allowed = EDITABLE_FIELDS[table_type] | {"row_index", "page_number", "is_auto_generated"}
    result = []
    for i, raw in enumerate(rows):
        payload = {k: v for k, v in raw.items() if k in allowed and k != "id"}
        payload["project_id"] = project_id
        payload["row_index"] = payload.get("row_index", i)
        obj = model(**payload)
        db.add(obj)
        result.append(obj)
    db.commit()
    return get_rows(db, project_id, table_type)


def patch_rows(db: Session, project_id: int, table_type: str, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Upsert by id when present; otherwise full replace if no ids."""
    if not rows:
        return replace_rows(db, project_id, table_type, [])
    if all("id" in r and r["id"] for r in rows):
        model = ROW_MODELS[table_type]
        allowed = EDITABLE_FIELDS[table_type] | {"row_index", "page_number", "is_auto_generated"}
        for raw in rows:
            obj = db.get(model, raw["id"])
            if not obj or obj.project_id != project_id:
                continue
            for k, v in raw.items():
                if k in allowed:
                    setattr(obj, k, v)
        db.commit()
        return get_rows(db, project_id, table_type)
    return replace_rows(db, project_id, table_type, rows)


def generate_table(
    db: Session,
    project: Project,
    table_type: str,
    name_field: str | None = None,
    tech_reserve_percent: float | None = None,
    boards_count: int | None = None,
) -> list[dict[str, Any]]:
    if table_type not in TABLE_TYPES:
        raise ValueError(f"Unknown table type: {table_type}")

    mappings = _mappings_dict(db, project.id)
    components = load_components_for_variant(db, project)

    if table_type == "perechen":
        rows = generate_perechen(components, name_field=name_field or "Name", mappings=mappings)
    elif table_type == "specification_pcb":
        rows = generate_specification_pcb(
            components,
            decimal_number=project.decimal_number,
            board_name=project.board_name,
            pcb_doc_name=project.pcb_doc_name or "",
            mappings=mappings,
        )
    elif table_type == "specification":
        materials: list[MaterialView] = []
        layer_count = 0
        if project.pcb_data:
            layer_count = project.pcb_data.layer_count
            for m in project.pcb_data.diel_materials:
                materials.append(
                    MaterialView(
                        name=m.name,
                        value=m.value,
                        height=m.height,
                        diel_type=m.diel_type,
                        layer_number=m.layer_number,
                    )
                )
        rows = generate_specification(materials, layer_count=layer_count, decimal_number=project.decimal_number)
    elif table_type == "vedomost":
        inscriptions = {
            i.field_number: i.field_value
            for i in db.scalars(
                select(TitleInscription).where(TitleInscription.project_id == project.id)
            ).all()
        }
        where_used = inscriptions.get(101) or inscriptions.get(1001) or project.decimal_number
        rows = generate_vedomost(components, where_used=where_used, mappings=mappings)
    else:
        rows = generate_simple_list(
            components,
            name_field=name_field or "Name",
            tech_reserve_percent=tech_reserve_percent if tech_reserve_percent is not None else 10.0,
            boards_count=boards_count if boards_count is not None else 1,
        )

    return replace_rows(db, project.id, table_type, rows)


def get_inscriptions(db: Session, project_id: int) -> dict[int, str]:
    rows = db.scalars(
        select(TitleInscription).where(TitleInscription.project_id == project_id)
    ).all()
    return {r.field_number: r.field_value for r in rows}


def set_inscriptions(db: Session, project_id: int, values: dict[int, str]) -> dict[int, str]:
    existing = {
        r.field_number: r
        for r in db.scalars(
            select(TitleInscription).where(TitleInscription.project_id == project_id)
        ).all()
    }
    for num, val in values.items():
        num = int(num)
        if num in existing:
            existing[num].field_value = val
        else:
            db.add(TitleInscription(project_id=project_id, field_number=num, field_value=val))
    db.commit()
    return get_inscriptions(db, project_id)


def apply_llm_edits(
    db: Session, project_id: int, table_type: str, edits: list[dict[str, Any]]
) -> list[dict[str, Any]]:
    model = ROW_MODELS[table_type]
    allowed = EDITABLE_FIELDS[table_type]
    for edit in edits:
        op = edit.get("op", "update_row")
        if op == "update_row":
            obj = None
            if edit.get("row_id"):
                obj = db.get(model, edit["row_id"])
            elif edit.get("row_index") is not None:
                obj = db.scalar(
                    select(model).where(
                        model.project_id == project_id,
                        model.row_index == edit["row_index"],
                    )
                )
            if not obj or obj.project_id != project_id:
                continue
            for k, v in (edit.get("fields") or {}).items():
                if k in allowed:
                    setattr(obj, k, v)
        elif op == "delete_row":
            obj = None
            if edit.get("row_id"):
                obj = db.get(model, edit["row_id"])
            if obj and obj.project_id == project_id:
                db.delete(obj)
        elif op == "add_row":
            payload = {k: v for k, v in (edit.get("fields") or {}).items() if k in allowed}
            payload["project_id"] = project_id
            payload["row_index"] = edit.get("row_index", 9999)
            payload["is_auto_generated"] = False
            db.add(model(**payload))
    db.commit()
    # reindex
    rows = db.scalars(
        select(model).where(model.project_id == project_id).order_by(model.row_index)
    ).all()
    for i, r in enumerate(rows):
        r.row_index = i
    db.commit()
    return get_rows(db, project_id, table_type)
