"""Babofkátor — hlavní výpočetní řetězec.

    STL  ->  analýza úkosů  ->  výšková mapa (horní obálka)
         ->  Lipschitzova korekce úkosů
         ->  korigovaný model (STL)  +  inverzní forma s lemem (STL)
"""
from __future__ import annotations

import time
from dataclasses import dataclass, field, asdict

import numpy as np

from . import analysis, draft as draftmod, meshing, mold as moldmod, raster
from .raster import Grid
from .stlio import read_mesh, stl_bytes
from .validate import check_mesh


# --------------------------------------------------------------------------- #
@dataclass
class Params:
    draft_deg: float = 3.0          # požadovaný úhel úkosu (od osy vyjmutí)
    wall_mm: float = 3.0            # tloušťka stěny formy
    rim_mm: float = 5.0             # šířka lemu
    resolution_mm: float = 0.0      # 0 = automaticky
    target_cells: int = 1200        # cíl pro auto-rozlišení (delší strana)
    max_cells: int = 3000           # tvrdý strop velikosti mřížky
    min_res_mm: float = 0.02        # jemnější už nemá smysl (FDM/SLA)
    mode: str = "add"               # add | cut | balanced
    back: str = "shell"             # shell | flat
    smooth_px: float = 0.0
    safety: float = 1.03
    mesh_tol_mm: float = 0.03      # tolerance adaptivního zjednodušení sítě
    rim_smooth_mm: float = -1.0     # zjednodušení obrysu lemu; -1 = = šířka lemu
    up_axis: str = "auto"           # auto | z | y | x  (osa vyjmutí)
    plane_z: float | None = None    # None = automaticky min(z)
    clip_below_plane: bool = True
    make_model: bool = True
    make_mold: bool = True

    def to_dict(self):
        return asdict(self)


@dataclass
class Result:
    params: dict = field(default_factory=dict)
    report: dict = field(default_factory=dict)
    grid: Grid | None = None
    h0: np.ndarray | None = None
    h: np.ndarray | None = None
    covered: np.ndarray | None = None
    hits: np.ndarray | None = None
    model_mesh: tuple | None = None
    mold_mesh: tuple | None = None
    timings: dict = field(default_factory=dict)


def guess_up_axis(V: np.ndarray) -> str:
    """Odhadne osu vyjmutí: reliéf leží na rovině, takže podél správné osy
    je u minima nahromaděno nejvíc vrcholů (ploché dno) a rozsah je nejmenší."""
    names = "xyz"
    ext = V.max(axis=0) - V.min(axis=0)
    score = []
    for k in range(3):
        e = max(float(ext[k]), 1e-9)
        flat = float(np.mean(np.abs(V[:, k] - V[:, k].min()) < 1e-4 * e))
        score.append(flat + 0.25 * (1.0 - e / max(float(ext.max()), 1e-9)))
    return names[int(np.argmax(score))]


def orient_up(V: np.ndarray, axis: str) -> np.ndarray:
    """Otočí model tak, aby osou vyjmutí bylo +Z."""
    a = (axis or "z").lower()
    if a == "z":
        return V
    if a == "y":                       # Y-up (typické pro OBJ)
        return np.stack([V[:, 0], -V[:, 2], V[:, 1]], axis=1)
    if a == "x":
        return np.stack([V[:, 1], V[:, 2], V[:, 0]], axis=1)
    return V


def _auto_resolution(vmin, vmax, p: Params) -> tuple[float, float]:
    """Vrátí (res, pad).  Rozlišení se volí z rozměru **včetně** lemu a
    bočního dosahu korekce, aby výsledná mřížka nepřerostla max_cells."""
    ext = max(float(vmax[0] - vmin[0]), float(vmax[1] - vmin[1]), 1e-6)
    s_req = draftmod.slope_from_draft(p.draft_deg)
    h_range = float(max(vmax[2] - vmin[2], 0.0))
    rs = p.rim_mm if p.rim_smooth_mm < 0 else float(p.rim_smooth_mm)
    pad = p.rim_mm + max(rs, 0.0) + p.wall_mm + \
        h_range / max(s_req, 1e-6) + 1.0
    total = ext + 2.0 * pad

    if p.resolution_mm and p.resolution_mm > 0:
        res = float(p.resolution_mm)
    else:
        res = total / max(64, int(p.target_cells))
    res = max(res, total / max(64, int(p.max_cells)), float(p.min_res_mm))
    return float(res), float(pad + 4.0 * res)


def run(stl_source, p: Params | None = None, progress=None) -> Result:
    p = p or Params()
    log = progress or (lambda *a, **k: None)
    t_all = time.time()
    T = {}

    # ---- 1. načtení -------------------------------------------------------
    t = time.time()
    log(2, "Načítám STL…")
    V, F = read_mesh(stl_source, getattr(p, "_name", None))
    if F.shape[0] == 0:
        raise ValueError("Soubor neobsahuje žádné použitelné trojúhelníky.")
    axis = guess_up_axis(V) if p.up_axis == "auto" else p.up_axis
    V = orient_up(V, axis)
    T["load"] = time.time() - t

    vmin = V.min(axis=0)
    vmax = V.max(axis=0)
    z_plane = float(vmin[2]) if p.plane_z is None else float(p.plane_z)
    V = V.copy()
    V[:, 2] -= z_plane
    vmin = V.min(axis=0)
    vmax = V.max(axis=0)

    # ---- 2. analýza úkosů původního modelu -------------------------------
    t = time.time()
    log(8, "Analyzuji sklony stěn…")
    an = analysis.analyze(V, F, p.draft_deg)
    T["analysis"] = time.time() - t

    # ---- 3. rasterizace horní obálky --------------------------------------
    res, pad = _auto_resolution(vmin, vmax, p)
    grid = raster.make_grid(vmin, vmax, res, pad)
    log(15, f"Mřížka {grid.nx}×{grid.ny} px, krok {res:.3f} mm…")

    t = time.time()
    hf = raster.height_field(V, F, grid, z_plane=0.0)
    T["raster"] = time.time() - t
    h0 = hf["h"]
    covered = hf["covered"]
    hits = hf["hits"]

    vol_mesh = abs(raster.mesh_volume(V, F))
    vol_env = float(h0.sum()) * grid.cell_area
    T["raster"] = time.time() - t

    # ---- 4. korekce úkosů --------------------------------------------------
    t = time.time()
    log(35, "Koriguji podkosy a úkosy…")
    h = draftmod.correct_draft(h0, grid.res, p.draft_deg, mode=p.mode,
                               safety=p.safety, smooth_px=p.smooth_px)
    T["draft"] = time.time() - t
    achieved = draftmod.achieved_draft(h, grid.res)

    dh = h - h0
    added = float(np.clip(dh, 0, None).sum()) * grid.cell_area
    removed = float(np.clip(-dh, 0, None).sum()) * grid.cell_area

    # ---- 5. korigovaný model ----------------------------------------------
    model_mesh = None
    model_check = None
    if p.make_model:
        t = time.time()
        log(55, "Sestavuji korigovaný model…")
        eps = 1e-9
        node = h > eps
        cm = meshing.cells_from_nodes(node)
        Vm, Fm = meshing.heightfields_to_mesh_adaptive(
            grid, np.zeros_like(h), h, cm, tol=p.mesh_tol_mm)
        model_mesh = (Vm, Fm)
        model_check = check_mesh(Vm, Fm)
        T["model_mesh"] = time.time() - t

    # ---- 6. inverzní forma -------------------------------------------------
    mold_mesh = None
    mold_check = None
    mold_info = {}
    if p.make_mold:
        t = time.time()
        log(70, "Generuji inverzní formu…")
        bottom, top, cmask, mold_info = moldmod.build_mold(
            grid, h, wall_mm=p.wall_mm, rim_mm=p.rim_mm, back=p.back,
            rim_smooth_mm=p.rim_smooth_mm)
        log(85, "Meshuji formu…")
        Vf, Ff = meshing.heightfields_to_mesh_adaptive(
            grid, bottom, top, cmask, tol=p.mesh_tol_mm)
        mold_mesh = (Vf, Ff)
        mold_check = check_mesh(Vf, Ff)
        T["mold"] = time.time() - t

    mesh_draft = None
    if model_mesh is not None:
        mesh_draft = analysis.min_draft_on_model(*model_mesh)
    mold_draft = None
    if mold_mesh is not None:
        mold_draft = analysis.min_draft_on_mold(*mold_mesh)

    T["total"] = time.time() - t_all
    log(98, "Hotovo.")

    report = {
        "input": {
            "n_faces": int(F.shape[0]),
            "n_vertices": int(V.shape[0]),
            "bbox_min": vmin.tolist(),
            "bbox_max": vmax.tolist(),
            "size_mm": (vmax - vmin).tolist(),
            "parting_plane_z_original": z_plane,
            "up_axis": axis,
            "volume_mm3": vol_mesh,
        },
        "draft_analysis": {k: v for k, v in an.items()
                           if not isinstance(v, np.ndarray)},
        "grid": {"nx": grid.nx, "ny": grid.ny, "res_mm": grid.res},
        "undercuts": {
            "local_undercut_faces": an["n_undercut_faces"],
            "local_undercut_area_pct": an["area_undercut_pct"],
            "insufficient_draft_area_pct": an["area_insufficient_pct"],
            "multilayer_px": int((hits > 2).sum()),
            "multilayer_pct": float((hits > 2).sum() /
                                    max(1, int(covered.sum())) * 100.0),
            "volume_under_upper_envelope_mm3": vol_env,
            "volume_filled_by_envelope_mm3": max(0.0, vol_env - vol_mesh),
        },
        "correction": {
            "mode": p.mode,
            "required_draft_deg": p.draft_deg,
            "achieved_min_draft_deg": achieved,
            "min_draft_on_model_mesh_deg": mesh_draft,
            "min_draft_in_mold_cavity_deg": mold_draft,
            "material_added_mm3": added,
            "material_removed_mm3": removed,
            "max_height_change_mm": float(np.abs(dh).max()) if dh.size else 0.0,
            "changed_area_pct": float((np.abs(dh) > 1e-6).sum() /
                                      max(1, h.size) * 100.0),
        },
        "mold": mold_info,
        "mesh_checks": {"model": model_check, "mold": mold_check},
        "timings_s": {k: round(v, 3) for k, v in T.items()},
    }

    return Result(params=p.to_dict(), report=report, grid=grid, h0=h0, h=h,
                  covered=covered, hits=hits, model_mesh=model_mesh,
                  mold_mesh=mold_mesh, timings=T)


# --------------------------------------------------------------------------- #
def export(result: Result, out_dir: str, stem: str = "model"):
    import os
    os.makedirs(out_dir, exist_ok=True)
    paths = {}
    if result.model_mesh is not None:
        pth = os.path.join(out_dir, f"{stem}_korigovany.stl")
        with open(pth, "wb") as fh:
            fh.write(stl_bytes(*result.model_mesh, header="Babofkator model"))
        paths["model"] = pth
    if result.mold_mesh is not None:
        pth = os.path.join(out_dir, f"{stem}_forma.stl")
        with open(pth, "wb") as fh:
            fh.write(stl_bytes(*result.mold_mesh, header="Babofkator forma"))
        paths["mold"] = pth
    return paths
