"""Perhitungan skala mikroskop: µm per piksel dan label perbesaran.

Satu sumber kebenaran untuk angka skala ada di server. Penampil (JS) hanya
menerima `um_per_px` lalu memilih panjang bar yang enak dilihat; ia tidak
pernah menghitung ulang kalibrasi.
"""

from __future__ import annotations

import math

KANDIDAT_UM = (5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000)
BAR_MIN_PX = 60
BAR_MAKS_PX = 160
BAR_TARGET_PX = 100


class KalibrasiTidakSah(ValueError):
    """Garis kalibrasi atau panjang µm tidak masuk akal."""


def _angka(nilai) -> float:
    try:
        hasil = float(nilai)
    except (TypeError, ValueError) as exc:
        raise KalibrasiTidakSah("Panjang garis dan µm harus berupa angka.") from exc
    if math.isnan(hasil) or math.isinf(hasil):
        raise KalibrasiTidakSah("Panjang garis dan µm harus berupa angka nyata.")
    return hasil


def hitung_um_per_px(panjang_um, panjang_px) -> float:
    """µm per piksel gambar dari panjang bar dan panjangnya di gambar.

    Raises:
        KalibrasiTidakSah: garis ≤ 1 px, µm ≤ 0, atau bukan angka.
    """
    um = _angka(panjang_um)
    px = _angka(panjang_px)
    if um <= 0:
        raise KalibrasiTidakSah("Panjang skala (µm) harus lebih besar dari nol.")
    if px <= 1:
        raise KalibrasiTidakSah(
            "Garis kalibrasi terlalu pendek — tarik garis di atas skala bar foto.")
    return round(um / px, 5)


def bar_disarankan(um_per_px, _lebar_layar_px: int | None = None) -> tuple[int, float]:
    """Panjang bar (µm bulat, lebar layar px) yang paling enak dibaca.

    ``_lebar_layar_px`` diterima demi tanda tangan pemanggil, tetapi tidak
    dipakai: lebar layar yang sebenarnya baru diketahui penampil di peramban,
    jadi di sini diasumsikan skala 1:1 dan JS yang menyesuaikan.

    Raises:
        KalibrasiTidakSah: `um_per_px` tidak sah.
    """
    um_px = _angka(um_per_px)
    if um_px <= 0:
        raise KalibrasiTidakSah("Skala belum dikalibrasi.")
    pilihan = [(um, um / um_px) for um in KANDIDAT_UM]
    pas = [p for p in pilihan if BAR_MIN_PX <= p[1] <= BAR_MAKS_PX]
    if pas:
        return pas[0]
    terdekat = min(pilihan, key=lambda p: abs(p[1] - BAR_TARGET_PX))
    return terdekat


def _desimal(nilai: float, digit: int = 2) -> str:
    return f"{nilai:.{digit}f}".replace(".", ",")


def label_skala(gambar: dict) -> str:
    """Kalimat keterangan perbesaran + skala untuk ditampilkan di penampil."""
    perbesaran = gambar.get("perbesaran_total")
    um_px = gambar.get("um_per_px")
    bagian: list[str] = []
    if perbesaran:
        teks = f"Diambil pada {perbesaran}x"
        objektif = (gambar.get("objektif") or "").strip()
        okuler = gambar.get("okuler")
        if objektif and okuler:
            teks += f" (objektif {objektif} × okuler {okuler})"
        elif objektif:
            teks += f" (objektif {objektif})"
        bagian.append(teks)
    if um_px:
        bagian.append(f"{_desimal(float(um_px))} µm/px")
    else:
        bagian.append("skala belum dikalibrasi")
    return " · ".join(bagian) if bagian else "skala belum dikalibrasi"
