from flask import (
    render_template,
    redirect,
    session,
    request,
    send_file,
    flash
)

from extensions import app, mysql

from datetime import datetime, timedelta

from openpyxl import Workbook
from openpyxl.styles import (
    Font,
    PatternFill,
    Alignment,
    Border,
    Side
)
from openpyxl.utils import get_column_letter

import io


# ==========================================================
# CEK AKSES ADMIN
# ==========================================================
def cek_akses_admin_ann():

    if "login" not in session:
        return False

    if session.get("role") != "admin":
        return False

    return True


# ==========================================================
# DATASET ANN - SINKRON DENGAN MODEL TRAINING
# ==========================================================
def _nama_hari_indonesia(index_hari):
    nama = [
        "Senin", "Selasa", "Rabu", "Kamis",
        "Jumat", "Sabtu", "Minggu"
    ]
    try:
        return nama[int(index_hari)]
    except Exception:
        return "-"


def _siapkan_dataset_ann_tampilan(
    barang_id="",
    tanggal_awal="",
    tanggal_akhir=""
):
    """
    Membentuk dataset yang SAMA dengan data yang masuk ke ANN.

    Fitur:
    X1 = keluar_lag_1
    X2 = keluar_lag_2
    X3 = keluar_lag_3
    X4 = keluar_lag_7
    X5 = rata_keluar_3_hari
    X6 = rata_keluar_7_hari
    X7 = hari_dalam_minggu

    Target:
    Y = barang_keluar pada tanggal target.
    """
    sumber = _ambil_dataset_stok_harian()

    tanggal_filter_awal = None
    tanggal_filter_akhir = None

    if tanggal_awal:
        tanggal_filter_awal = datetime.strptime(
            tanggal_awal, "%Y-%m-%d"
        ).date()

    if tanggal_akhir:
        tanggal_filter_akhir = datetime.strptime(
            tanggal_akhir, "%Y-%m-%d"
        ).date()

    if (
        tanggal_filter_awal
        and tanggal_filter_akhir
        and tanggal_filter_awal > tanggal_filter_akhir
    ):
        raise ValueError(
            "Tanggal awal tidak boleh lebih besar dari tanggal akhir."
        )

    # Pembagian train/test harus sama dengan fungsi training ANN:
    # 80% tanggal target paling awal = training,
    # 20% tanggal target paling akhir = testing.
    semua_tanggal_target = []

    for rows in sumber["dataset_per_barang"].values():
        for i in range(7, len(rows)):
            semua_tanggal_target.append(rows[i]["tanggal"])

    tanggal_unik = sorted(set(semua_tanggal_target))

    if len(tanggal_unik) < 10:
        raise ValueError(
            "Jumlah hari efektif untuk dataset ANN terlalu sedikit."
        )

    index_batas = max(1, int(len(tanggal_unik) * 0.80))

    if index_batas >= len(tanggal_unik):
        index_batas = len(tanggal_unik) - 1

    tanggal_batas = tanggal_unik[index_batas]

    dataset = []

    for id_barang, rows in sumber["dataset_per_barang"].items():
        if barang_id and str(id_barang) != str(barang_id):
            continue

        for i in range(7, len(rows)):
            target = rows[i]
            tanggal_target = target["tanggal"]

            if (
                tanggal_filter_awal
                and tanggal_target < tanggal_filter_awal
            ):
                continue

            if (
                tanggal_filter_akhir
                and tanggal_target > tanggal_filter_akhir
            ):
                continue

            keluar_3 = [
                rows[i - j]["barang_keluar"]
                for j in range(1, 4)
            ]

            keluar_7 = [
                rows[i - j]["barang_keluar"]
                for j in range(1, 8)
            ]

            hari_index = tanggal_target.weekday()

            dataset.append({
                "tanggal": tanggal_target,
                "barang_id": id_barang,
                "kode_barang": target["kode_barang"],
                "nama_barang": target["nama_barang"],
                "satuan_kecil": target["satuan_kecil"],

                # Kondisi stok pada tanggal target.
                "stok_awal": int(target["stok_awal"]),
                "barang_masuk": int(target["barang_masuk"]),
                "barang_keluar": int(target["barang_keluar"]),
                "stok_akhir": int(target["stok_akhir"]),

                # Input ANN.
                "keluar_lag_1": int(rows[i - 1]["barang_keluar"]),
                "keluar_lag_2": int(rows[i - 2]["barang_keluar"]),
                "keluar_lag_3": int(rows[i - 3]["barang_keluar"]),
                "keluar_lag_7": int(rows[i - 7]["barang_keluar"]),
                "rata_keluar_3_hari": float(np.mean(keluar_3)),
                "rata_keluar_7_hari": float(np.mean(keluar_7)),
                "hari_index": int(hari_index),
                "hari_nama": _nama_hari_indonesia(hari_index),

                # Target ANN.
                "target_barang_keluar": int(
                    target["barang_keluar"]
                ),

                "jenis_data": (
                    "Training"
                    if tanggal_target < tanggal_batas
                    else "Testing"
                )
            })

    dataset.sort(
        key=lambda x: (
            x["tanggal"],
            x["nama_barang"]
        ),
        reverse=True
    )

    return dataset, sumber, tanggal_batas


def _contoh_perhitungan_ann(dataset):
    """
    Menyiapkan satu record contoh untuk penjelasan perhitungan ANN manual.
    Jika model sudah tersedia, tampilkan juga nilai StandardScaler
    berdasarkan scaler yang benar-benar dipakai model.
    """
    if not dataset:
        return None

    # Utamakan data Testing agar contoh juga bisa dibandingkan
    # dengan hasil model yang sudah dilatih.
    contoh = next(
        (
            item for item in dataset
            if item["jenis_data"] == "Testing"
        ),
        dataset[0]
    )

    nilai_input = np.asarray([[
        contoh["keluar_lag_1"],
        contoh["keluar_lag_2"],
        contoh["keluar_lag_3"],
        contoh["keluar_lag_7"],
        contoh["rata_keluar_3_hari"],
        contoh["rata_keluar_7_hari"],
        contoh["hari_index"]
    ]], dtype=float)

    contoh = dict(contoh)
    contoh["fitur"] = [
        {
            "kode": "X1",
            "nama": "Barang Keluar Lag-1",
            "nilai": contoh["keluar_lag_1"]
        },
        {
            "kode": "X2",
            "nama": "Barang Keluar Lag-2",
            "nilai": contoh["keluar_lag_2"]
        },
        {
            "kode": "X3",
            "nama": "Barang Keluar Lag-3",
            "nilai": contoh["keluar_lag_3"]
        },
        {
            "kode": "X4",
            "nama": "Barang Keluar Lag-7",
            "nilai": contoh["keluar_lag_7"]
        },
        {
            "kode": "X5",
            "nama": "Rata-rata Barang Keluar 3 Hari",
            "nilai": round(
                contoh["rata_keluar_3_hari"], 4
            )
        },
        {
            "kode": "X6",
            "nama": "Rata-rata Barang Keluar 7 Hari",
            "nilai": round(
                contoh["rata_keluar_7_hari"], 4
            )
        },
        {
            "kode": "X7",
            "nama": "Hari dalam Minggu",
            "nilai": contoh["hari_index"]
        }
    ]

    contoh["model_tersedia"] = False
    contoh["fitur_normalisasi"] = []
    contoh["prediksi_model"] = None

    if os.path.exists(MODEL_ANN_PATH):
        try:
            bundle = joblib.load(MODEL_ANN_PATH)
            scaler_x = bundle["scaler_x"]
            scaler_y = bundle["scaler_y"]
            model = bundle["model"]

            nilai_normal = scaler_x.transform(
                nilai_input
            )[0]

            for index, fitur in enumerate(contoh["fitur"]):
                contoh["fitur_normalisasi"].append({
                    "kode": fitur["kode"],
                    "nama": fitur["nama"],
                    "nilai_asli": fitur["nilai"],
                    "mean": float(scaler_x.mean_[index]),
                    "std": float(scaler_x.scale_[index]),
                    "nilai_normalisasi": float(
                        nilai_normal[index]
                    )
                })

            pred_scaled = model.predict(
                nilai_normal.reshape(1, -1)
            )

            prediksi = scaler_y.inverse_transform(
                np.asarray(pred_scaled).reshape(-1, 1)
            ).ravel()[0]

            contoh["prediksi_model"] = max(
                0.0, float(prediksi)
            )

            # --------------------------------------------------
            # DETAIL PERHITUNGAN FORWARD PROPAGATION ANN
            # Dipakai sebagai contoh perhitungan manual BAB III.
            # Nilai diambil langsung dari bobot dan bias model
            # hasil training, sehingga bukan angka contoh buatan.
            # --------------------------------------------------
            x_normal = np.asarray(nilai_normal, dtype=float)

            w1 = np.asarray(model.coefs_[0], dtype=float)
            b1 = np.asarray(model.intercepts_[0], dtype=float)
            z1 = np.dot(x_normal, w1) + b1
            a1 = np.maximum(0.0, z1)

            w2 = np.asarray(model.coefs_[1], dtype=float)
            b2 = np.asarray(model.intercepts_[1], dtype=float)
            z2 = np.dot(a1, w2) + b2
            a2 = np.maximum(0.0, z2)

            w3 = np.asarray(model.coefs_[2], dtype=float)
            b3 = np.asarray(model.intercepts_[2], dtype=float)
            output_scaled_manual = float(
                (np.dot(a2, w3) + b3).ravel()[0]
            )

            output_asli_manual = float(
                scaler_y.inverse_transform(
                    np.asarray([[output_scaled_manual]], dtype=float)
                ).ravel()[0]
            )
            output_asli_manual = max(0.0, output_asli_manual)

            # Contoh rinci neuron pertama Hidden Layer 1.
            contoh["hidden1_neuron1"] = {
                "bias": float(b1[0]),
                "z": float(z1[0]),
                "aktivasi": float(a1[0]),
                "kontribusi": [
                    {
                        "kode": f"X{idx + 1}",
                        "input": float(x_normal[idx]),
                        "bobot": float(w1[idx, 0]),
                        "hasil_kali": float(x_normal[idx] * w1[idx, 0])
                    }
                    for idx in range(len(x_normal))
                ]
            }

            # Seluruh aktivasi H1 diperlukan untuk menghitung neuron H2.
            contoh["hidden1_ringkas"] = [
                {
                    "neuron": idx + 1,
                    "z": float(z1[idx]),
                    "aktivasi": float(a1[idx])
                }
                for idx in range(len(a1))
            ]

            # Contoh rinci neuron pertama Hidden Layer 2.
            contoh["hidden2_neuron1"] = {
                "bias": float(b2[0]),
                "z": float(z2[0]),
                "aktivasi": float(a2[0]),
                "kontribusi": [
                    {
                        "kode": f"H1-{idx + 1}",
                        "input": float(a1[idx]),
                        "bobot": float(w2[idx, 0]),
                        "hasil_kali": float(a1[idx] * w2[idx, 0])
                    }
                    for idx in range(len(a1))
                ]
            }

            contoh["hidden2_ringkas"] = [
                {
                    "neuron": idx + 1,
                    "z": float(z2[idx]),
                    "aktivasi": float(a2[idx])
                }
                for idx in range(len(a2))
            ]

            # Output memakai seluruh 16 aktivasi Hidden Layer 2.
            contoh["output_manual"] = {
                "bias": float(b3.ravel()[0]),
                "output_scaled": output_scaled_manual,
                "mean_y": float(scaler_y.mean_[0]),
                "std_y": float(scaler_y.scale_[0]),
                "output_asli": output_asli_manual,
                "kontribusi": [
                    {
                        "kode": f"H2-{idx + 1}",
                        "input": float(a2[idx]),
                        "bobot": float(w3[idx, 0]),
                        "hasil_kali": float(a2[idx] * w3[idx, 0])
                    }
                    for idx in range(len(a2))
                ]
            }

            contoh["error_absolut"] = abs(
                float(contoh["target_barang_keluar"])
                - output_asli_manual
            )
            contoh["model_tersedia"] = True

        except Exception as e:
            contoh["model_error"] = str(e)

    return contoh


@app.route("/admin/dataset-ann")
def dataset_ann():
    if not cek_akses_admin_ann():
        return redirect("/login")

    barang_id = request.args.get(
        "barang_id", ""
    ).strip()

    tanggal_awal = request.args.get(
        "tanggal_awal", ""
    ).strip()

    tanggal_akhir = request.args.get(
        "tanggal_akhir", ""
    ).strip()

    cursor = mysql.connection.cursor()

    try:
        cursor.execute(
            """
            SELECT
                id,
                kode_barang,
                nama_barang,
                satuan_kecil
            FROM barang
            ORDER BY nama_barang ASC
            """
        )

        data_barang = cursor.fetchall()
        cursor.close()

        barang = [
            {
                "id": item[0],
                "kode_barang": item[1],
                "nama_barang": item[2],
                "satuan_kecil": item[3]
            }
            for item in data_barang
        ]

        dataset, sumber, tanggal_batas = (
            _siapkan_dataset_ann_tampilan(
                barang_id=barang_id,
                tanggal_awal=tanggal_awal,
                tanggal_akhir=tanggal_akhir
            )
        )

        jumlah_record = len(dataset)
        jumlah_training = sum(
            1 for item in dataset
            if item["jenis_data"] == "Training"
        )
        jumlah_testing = sum(
            1 for item in dataset
            if item["jenis_data"] == "Testing"
        )
        jumlah_barang_dataset = len({
            item["barang_id"]
            for item in dataset
        })

        contoh_perhitungan = _contoh_perhitungan_ann(
            dataset
        )

        return render_template(
            "admin/dataset_ann.html",
            dataset=dataset,
            barang=barang,
            barang_id=barang_id,
            tanggal_awal=tanggal_awal,
            tanggal_akhir=tanggal_akhir,
            jumlah_record=jumlah_record,
            jumlah_training=jumlah_training,
            jumlah_testing=jumlah_testing,
            jumlah_barang_dataset=jumlah_barang_dataset,
            tanggal_data_awal=sumber["tanggal_awal"],
            tanggal_data_akhir=sumber["tanggal_akhir"],
            tanggal_batas=tanggal_batas,
            contoh_perhitungan=contoh_perhitungan,
            model_info=_status_model_ann()
        )

    except Exception as e:
        try:
            cursor.close()
        except Exception:
            pass

        print("ERROR DATASET ANN:", e)

        flash(
            f"Dataset ANN gagal dibuka: {e}",
            "danger"
        )

        return redirect("/admin")


# ==========================================================
# EXPORT DATASET ANN - DATA YANG BENAR-BENAR MASUK KE MODEL
# ==========================================================
@app.route("/admin/dataset-ann/export")
def export_dataset_ann():
    if not cek_akses_admin_ann():
        return redirect("/login")

    barang_id = request.args.get(
        "barang_id", ""
    ).strip()

    tanggal_awal = request.args.get(
        "tanggal_awal", ""
    ).strip()

    tanggal_akhir = request.args.get(
        "tanggal_akhir", ""
    ).strip()

    try:
        dataset, sumber, tanggal_batas = (
            _siapkan_dataset_ann_tampilan(
                barang_id=barang_id,
                tanggal_awal=tanggal_awal,
                tanggal_akhir=tanggal_akhir
            )
        )

        if not dataset:
            raise ValueError(
                "Tidak ada dataset ANN sesuai filter."
            )

        workbook = Workbook()
        sheet = workbook.active
        sheet.title = "Dataset ANN"

        headers = [
            "tanggal_target",
            "barang_id",
            "kode_barang",
            "nama_barang",
            "satuan_kecil",
            "stok_awal",
            "barang_masuk",
            "barang_keluar",
            "stok_akhir",
            "X1_keluar_lag_1",
            "X2_keluar_lag_2",
            "X3_keluar_lag_3",
            "X4_keluar_lag_7",
            "X5_rata_keluar_3_hari",
            "X6_rata_keluar_7_hari",
            "X7_hari_dalam_minggu",
            "nama_hari",
            "Y_target_barang_keluar",
            "jenis_data"
        ]

        sheet.merge_cells(
            start_row=1,
            start_column=1,
            end_row=1,
            end_column=len(headers)
        )
        sheet.cell(1, 1).value = (
            "DATASET ANN PREDIKSI BARANG KELUAR "
            "- TOKO GROSIR CUCU"
        )
        sheet.cell(1, 1).font = Font(
            bold=True,
            color="FFFFFF",
            size=14
        )
        sheet.cell(1, 1).fill = PatternFill(
            "solid",
            fgColor="166534"
        )
        sheet.cell(1, 1).alignment = Alignment(
            horizontal="center",
            vertical="center"
        )

        sheet.merge_cells(
            start_row=2,
            start_column=1,
            end_row=2,
            end_column=len(headers)
        )
        sheet.cell(2, 1).value = (
            "Fitur ANN: Lag-1, Lag-2, Lag-3, Lag-7, "
            "rata-rata 3 hari, rata-rata 7 hari, "
            "dan hari dalam minggu. "
            "Target Y = Barang Keluar pada tanggal target."
        )
        sheet.cell(2, 1).alignment = Alignment(
            horizontal="center",
            wrap_text=True
        )

        for col, header in enumerate(headers, start=1):
            cell = sheet.cell(4, col)
            cell.value = header
            cell.font = Font(
                bold=True,
                color="FFFFFF"
            )
            cell.fill = PatternFill(
                "solid",
                fgColor="15803D"
            )
            cell.alignment = Alignment(
                horizontal="center",
                vertical="center",
                wrap_text=True
            )

        border_tipis = Border(
            left=Side(style="thin", color="D1D5DB"),
            right=Side(style="thin", color="D1D5DB"),
            top=Side(style="thin", color="D1D5DB"),
            bottom=Side(style="thin", color="D1D5DB")
        )

        for row_index, item in enumerate(
            dataset, start=5
        ):
            values = [
                item["tanggal"],
                item["barang_id"],
                item["kode_barang"],
                item["nama_barang"],
                item["satuan_kecil"],
                item["stok_awal"],
                item["barang_masuk"],
                item["barang_keluar"],
                item["stok_akhir"],
                item["keluar_lag_1"],
                item["keluar_lag_2"],
                item["keluar_lag_3"],
                item["keluar_lag_7"],
                item["rata_keluar_3_hari"],
                item["rata_keluar_7_hari"],
                item["hari_index"],
                item["hari_nama"],
                item["target_barang_keluar"],
                item["jenis_data"]
            ]

            for col, value in enumerate(
                values, start=1
            ):
                cell = sheet.cell(
                    row=row_index,
                    column=col
                )
                cell.value = value
                cell.border = border_tipis
                cell.alignment = Alignment(
                    vertical="center"
                )

            sheet.cell(
                row=row_index,
                column=1
            ).number_format = "dd/mm/yyyy"

        for col in range(1, len(headers) + 1):
            sheet.cell(
                row=4,
                column=col
            ).border = border_tipis

        widths = {
            "A": 16, "B": 11, "C": 16, "D": 28,
            "E": 15, "F": 12, "G": 14, "H": 14,
            "I": 12, "J": 16, "K": 16, "L": 16,
            "M": 16, "N": 22, "O": 22, "P": 22,
            "Q": 14, "R": 24, "S": 14
        }

        for col, width in widths.items():
            sheet.column_dimensions[col].width = width

        sheet.freeze_panes = "A5"
        sheet.auto_filter.ref = (
            f"A4:S{4 + len(dataset)}"
        )

        # Sheet informasi metodologi.
        info = workbook.create_sheet(
            "Informasi Metode"
        )

        info_data = [
            ("Metode", "Artificial Neural Network / MLPRegressor"),
            ("Target", "Barang Keluar pada tanggal target"),
            ("X1", "Barang Keluar Lag-1"),
            ("X2", "Barang Keluar Lag-2"),
            ("X3", "Barang Keluar Lag-3"),
            ("X4", "Barang Keluar Lag-7"),
            ("X5", "Rata-rata Barang Keluar 3 Hari"),
            ("X6", "Rata-rata Barang Keluar 7 Hari"),
            ("X7", "Hari dalam Minggu (Senin=0 s.d. Minggu=6)"),
            ("Hidden Layer", "32 neuron dan 16 neuron"),
            ("Aktivasi", "ReLU"),
            ("Optimizer/Solver", "Adam"),
            ("Scaling", "StandardScaler untuk X dan y"),
            ("Pembagian Data", "Kronologis 80% training dan 20% testing"),
            (
                "Batas Testing",
                tanggal_batas.strftime("%d/%m/%Y")
            ),
            (
                "Periode Histori",
                f"{sumber['tanggal_awal'].strftime('%d/%m/%Y')} "
                f"s.d. {sumber['tanggal_akhir'].strftime('%d/%m/%Y')}"
            ),
            (
                "Catatan",
                "Barang Masuk dan stok historis digunakan untuk "
                "rekonstruksi kondisi persediaan. Target ANN tetap "
                "Barang Keluar."
            )
        ]

        info["A1"] = "INFORMASI METODOLOGI DATASET ANN"
        info["A1"].font = Font(
            bold=True,
            color="FFFFFF",
            size=14
        )
        info["A1"].fill = PatternFill(
            "solid",
            fgColor="166534"
        )
        info.merge_cells("A1:B1")

        for row_index, (label, value) in enumerate(
            info_data, start=3
        ):
            info.cell(row_index, 1).value = label
            info.cell(row_index, 1).font = Font(
                bold=True
            )
            info.cell(row_index, 2).value = value
            info.cell(row_index, 2).alignment = Alignment(
                wrap_text=True,
                vertical="top"
            )

        info.column_dimensions["A"].width = 24
        info.column_dimensions["B"].width = 75

        output = io.BytesIO()
        workbook.save(output)
        output.seek(0)

        nama_output = (
            "Dataset_ANN_Prediksi_Barang_Keluar_"
            f"{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
        )

        return send_file(
            output,
            as_attachment=True,
            download_name=nama_output,
            mimetype=(
                "application/"
                "vnd.openxmlformats-officedocument."
                "spreadsheetml.sheet"
            )
        )

    except Exception as e:
        print("ERROR EXPORT DATASET ANN:", e)

        flash(
            f"Dataset ANN gagal diexport: {e}",
            "danger"
        )

        return redirect("/admin/dataset-ann")


# ==========================================================
# ANN PREDIKSI KEBUTUHAN BARANG
# ==========================================================
# Tujuan model:
# - ANN mempelajari pola histori barang keluar / kebutuhan.
# - Target ANN adalah jumlah barang keluar hari berikutnya.
# - Histori barang masuk tetap dipakai untuk rekonstruksi stok historis.
# - Proyeksi stok dihitung dari stok aktual dikurangi prediksi kebutuhan.
# - Barang masuk masa depan tidak diasumsikan sebelum benar-benar tercatat.
# ==========================================================

import os
import joblib
import numpy as np

from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score


MODEL_ANN_PATH = os.path.join(
    "models",
    "model_ann_kebutuhan.pkl"
)


FITUR_ANN_STOK = [
    "keluar_lag_1",
    "keluar_lag_2",
    "keluar_lag_3",
    "keluar_lag_7",
    "rata_keluar_3_hari",
    "rata_keluar_7_hari",
    "hari_dalam_minggu"
]


def _normalisasi_tanggal(value):
    """Pastikan hasil DATE() MySQL menjadi datetime.date."""

    if value is None:
        return None

    if isinstance(value, datetime):
        return value.date()

    if isinstance(value, str):
        return datetime.strptime(
            value,
            "%Y-%m-%d"
        ).date()

    return value


def _ambil_dataset_stok_harian():
    """
    Membentuk histori harian seluruh barang.

    Stok historis direkonstruksi MUNDUR dari stok aktual saat ini.
    Cara ini lebih aman daripada menganggap stok awal periode = 0.

    Rumus dasar:
        stok_akhir_hari_ini
        = stok_akhir_hari_kemarin
          + barang_masuk
          - barang_keluar
    """

    cursor = mysql.connection.cursor()

    try:
        # ------------------------------------------------------
        # Rentang seluruh transaksi masuk dan keluar
        # ------------------------------------------------------
        cursor.execute(
            """
            SELECT
                LEAST(
                    COALESCE(
                        (SELECT MIN(DATE(tanggal)) FROM transaksi_masuk),
                        '9999-12-31'
                    ),
                    COALESCE(
                        (SELECT MIN(DATE(tanggal)) FROM transaksi_keluar),
                        '9999-12-31'
                    )
                ),
                GREATEST(
                    COALESCE(
                        (SELECT MAX(DATE(tanggal)) FROM transaksi_masuk),
                        '1000-01-01'
                    ),
                    COALESCE(
                        (SELECT MAX(DATE(tanggal)) FROM transaksi_keluar),
                        '1000-01-01'
                    )
                )
            """
        )

        rentang = cursor.fetchone()

        if not rentang:
            raise ValueError(
                "Belum ada histori transaksi masuk dan keluar."
            )

        tanggal_awal = _normalisasi_tanggal(rentang[0])
        tanggal_akhir = _normalisasi_tanggal(rentang[1])

        if (
            tanggal_awal is None
            or tanggal_akhir is None
            or tanggal_awal.year >= 9999
            or tanggal_akhir.year <= 1000
        ):
            raise ValueError(
                "Rentang histori transaksi belum tersedia."
            )

        # ------------------------------------------------------
        # Master barang + stok aktual
        # ------------------------------------------------------
        cursor.execute(
            """
            SELECT
                id,
                kode_barang,
                nama_barang,
                satuan_kecil,
                stok_kecil
            FROM barang
            ORDER BY id ASC
            """
        )

        master_barang = cursor.fetchall()

        if not master_barang:
            raise ValueError("Master barang masih kosong.")

        # ------------------------------------------------------
        # Total barang masuk per hari per barang
        # ------------------------------------------------------
        cursor.execute(
            """
            SELECT
                DATE(tm.tanggal),
                dtm.barang_id,
                COALESCE(SUM(dtm.jumlah_kecil), 0)
            FROM detail_transaksi_masuk dtm
            INNER JOIN transaksi_masuk tm
                ON dtm.transaksi_masuk_id = tm.id
            GROUP BY DATE(tm.tanggal), dtm.barang_id
            """
        )

        lookup_masuk = {}

        for row in cursor.fetchall():
            lookup_masuk[
                (row[1], _normalisasi_tanggal(row[0]))
            ] = int(row[2] or 0)

        # ------------------------------------------------------
        # Total barang keluar per hari per barang
        # ------------------------------------------------------
        cursor.execute(
            """
            SELECT
                DATE(tk.tanggal),
                dtk.barang_id,
                COALESCE(SUM(dtk.jumlah_kecil), 0)
            FROM detail_transaksi_keluar dtk
            INNER JOIN transaksi_keluar tk
                ON dtk.transaksi_keluar_id = tk.id
            GROUP BY DATE(tk.tanggal), dtk.barang_id
            """
        )

        lookup_keluar = {}

        for row in cursor.fetchall():
            lookup_keluar[
                (row[1], _normalisasi_tanggal(row[0]))
            ] = int(row[2] or 0)

        cursor.close()

        # ------------------------------------------------------
        # Daftar tanggal lengkap
        # ------------------------------------------------------
        semua_tanggal = []
        t = tanggal_awal

        while t <= tanggal_akhir:
            semua_tanggal.append(t)
            t += timedelta(days=1)

        dataset_per_barang = {}

        # ------------------------------------------------------
        # Rekonstruksi stok historis secara mundur
        # ------------------------------------------------------
        for b in master_barang:
            barang_id = int(b[0])
            stok_akhir_berjalan = int(b[4] or 0)
            rows_desc = []

            for tanggal in reversed(semua_tanggal):
                masuk = lookup_masuk.get(
                    (barang_id, tanggal),
                    0
                )

                keluar = lookup_keluar.get(
                    (barang_id, tanggal),
                    0
                )

                stok_akhir = stok_akhir_berjalan
                stok_awal = (
                    stok_akhir
                    - masuk
                    + keluar
                )

                # Data simulasi/riwayat tidak boleh menghasilkan
                # stok awal negatif. Jika terjadi, tandai agar
                # training tidak memakai histori yang tidak konsisten.
                if stok_awal < 0:
                    raise ValueError(
                        "Histori stok tidak konsisten pada "
                        f"{b[1]} - {tanggal.strftime('%d/%m/%Y')}. "
                        "Stok hasil rekonstruksi menjadi negatif."
                    )

                rows_desc.append({
                    "tanggal": tanggal,
                    "barang_id": barang_id,
                    "kode_barang": b[1],
                    "nama_barang": b[2],
                    "satuan_kecil": b[3],
                    "stok_awal": stok_awal,
                    "barang_masuk": masuk,
                    "barang_keluar": keluar,
                    "stok_akhir": stok_akhir
                })

                stok_akhir_berjalan = stok_awal

            dataset_per_barang[barang_id] = list(
                reversed(rows_desc)
            )

        return {
            "tanggal_awal": tanggal_awal,
            "tanggal_akhir": tanggal_akhir,
            "master_barang": master_barang,
            "dataset_per_barang": dataset_per_barang
        }

    except Exception:
        try:
            cursor.close()
        except Exception:
            pass
        raise


def _bentuk_data_training_ann():
    """
    Membentuk data training ANN untuk memprediksi kebutuhan barang.

    Target ANN = jumlah barang keluar pada hari berikutnya.
    Histori barang masuk tetap tersedia dalam rekonstruksi stok,
    tetapi tidak dianggap sebagai pasokan masa depan.
    """

    sumber = _ambil_dataset_stok_harian()

    X = []
    y = []
    tanggal_target = []

    for barang_id, rows in sumber["dataset_per_barang"].items():

        # Minimal 7 hari histori sebelum hari target.
        for i in range(7, len(rows)):
            target = rows[i]

            keluar_3 = [
                rows[i - j]["barang_keluar"]
                for j in range(1, 4)
            ]

            keluar_7 = [
                rows[i - j]["barang_keluar"]
                for j in range(1, 8)
            ]

            fitur = [
                rows[i - 1]["barang_keluar"],
                rows[i - 2]["barang_keluar"],
                rows[i - 3]["barang_keluar"],
                rows[i - 7]["barang_keluar"],
                float(np.mean(keluar_3)),
                float(np.mean(keluar_7)),
                target["tanggal"].weekday()
            ]

            X.append(fitur)

            # Target utama: kebutuhan / barang keluar harian.
            y.append(target["barang_keluar"])

            tanggal_target.append(target["tanggal"])

    if len(X) < 50:
        raise ValueError(
            "Data training ANN masih terlalu sedikit."
        )

    return (
        np.asarray(X, dtype=float),
        np.asarray(y, dtype=float),
        tanggal_target,
        sumber
    )


def _status_model_ann():
    """Baca metadata model jika model sudah pernah dilatih."""

    if not os.path.exists(MODEL_ANN_PATH):
        return None

    try:
        bundle = joblib.load(MODEL_ANN_PATH)

        return {
            "trained_at": bundle.get("trained_at"),
            "mae": bundle.get("mae"),
            "rmse": bundle.get("rmse"),
            "r2": bundle.get("r2"),
            "jumlah_data": bundle.get("jumlah_data"),
            "jumlah_train": bundle.get("jumlah_train"),
            "jumlah_test": bundle.get("jumlah_test"),
            "tanggal_awal": bundle.get("tanggal_awal"),
            "tanggal_akhir": bundle.get("tanggal_akhir")
        }

    except Exception:
        return None



# ==========================================================
# LAPORAN HASIL PENGUJIAN ANN
# ==========================================================
@app.route("/admin/laporan-ann")
def laporan_ann():
    if not cek_akses_admin_ann():
        return redirect("/login")

    try:
        if not os.path.exists(MODEL_ANN_PATH):
            return render_template(
                "admin/laporan_ann.html",
                model_info=None,
                hasil_testing=[]
            )

        bundle = joblib.load(MODEL_ANN_PATH)
        model = bundle["model"]
        scaler_x = bundle["scaler_x"]
        scaler_y = bundle["scaler_y"]

        # Gunakan dataset dan pembagian Training/Testing yang sama
        # dengan halaman Dataset ANN dan proses training.
        dataset, sumber, tanggal_batas = (
            _siapkan_dataset_ann_tampilan()
        )

        data_testing = [
            item for item in dataset
            if item["jenis_data"] == "Testing"
        ]

        hasil_testing = []

        for item in data_testing:
            fitur = np.asarray([[
                item["keluar_lag_1"],
                item["keluar_lag_2"],
                item["keluar_lag_3"],
                item["keluar_lag_7"],
                item["rata_keluar_3_hari"],
                item["rata_keluar_7_hari"],
                item["hari_index"]
            ]], dtype=float)

            fitur_scaled = scaler_x.transform(fitur)
            pred_scaled = model.predict(fitur_scaled)

            prediksi = scaler_y.inverse_transform(
                np.asarray(pred_scaled).reshape(-1, 1)
            ).ravel()[0]

            prediksi = max(0.0, float(prediksi))
            aktual = float(item["target_barang_keluar"])

            hasil_testing.append({
                "tanggal": item["tanggal"],
                "kode_barang": item["kode_barang"],
                "nama_barang": item["nama_barang"],
                "satuan_kecil": item["satuan_kecil"],
                "aktual": aktual,
                "prediksi": prediksi,
                "absolute_error": abs(aktual - prediksi)
            })

        # Dataset sudah urut terbaru terlebih dahulu.
        # Batasi tabel HTML agar halaman tetap ringan.
        jumlah_hasil_testing = len(hasil_testing)
        hasil_testing_tampil = hasil_testing[:200]

        return render_template(
            "admin/laporan_ann.html",
            model_info=_status_model_ann(),
            hasil_testing=hasil_testing_tampil,
            jumlah_hasil_testing=jumlah_hasil_testing,
            tanggal_batas=tanggal_batas,
            tanggal_data_awal=sumber["tanggal_awal"],
            tanggal_data_akhir=sumber["tanggal_akhir"]
        )

    except Exception as e:
        print("ERROR LAPORAN ANN:", e)

        flash(
            f"Laporan ANN gagal dibuka: {e}",
            "danger"
        )

        return redirect("/admin/prediksi-ann")



# ==========================================================
# EXPORT LAPORAN HASIL PENGUJIAN ANN
# ==========================================================
@app.route("/admin/laporan-ann/export")
def export_laporan_ann():
    if not cek_akses_admin_ann():
        return redirect("/login")

    try:
        if not os.path.exists(MODEL_ANN_PATH):
            raise ValueError(
                "Model ANN belum tersedia. Lakukan training terlebih dahulu."
            )

        bundle = joblib.load(MODEL_ANN_PATH)
        model = bundle["model"]
        scaler_x = bundle["scaler_x"]
        scaler_y = bundle["scaler_y"]

        dataset, sumber, tanggal_batas = (
            _siapkan_dataset_ann_tampilan()
        )

        data_testing = [
            item for item in dataset
            if item["jenis_data"] == "Testing"
        ]

        if not data_testing:
            raise ValueError("Data testing ANN tidak tersedia.")

        hasil_testing = []

        for item in data_testing:
            fitur = np.asarray([[
                item["keluar_lag_1"],
                item["keluar_lag_2"],
                item["keluar_lag_3"],
                item["keluar_lag_7"],
                item["rata_keluar_3_hari"],
                item["rata_keluar_7_hari"],
                item["hari_index"]
            ]], dtype=float)

            fitur_scaled = scaler_x.transform(fitur)
            pred_scaled = model.predict(fitur_scaled)

            prediksi = scaler_y.inverse_transform(
                np.asarray(pred_scaled).reshape(-1, 1)
            ).ravel()[0]

            prediksi = max(0.0, float(prediksi))
            aktual = float(item["target_barang_keluar"])

            hasil_testing.append({
                "tanggal": item["tanggal"],
                "kode_barang": item["kode_barang"],
                "nama_barang": item["nama_barang"],
                "satuan_kecil": item["satuan_kecil"],
                "aktual": aktual,
                "prediksi": prediksi,
                "absolute_error": abs(aktual - prediksi)
            })

        workbook = Workbook()

        # ------------------------------------------------------
        # SHEET 1 - RINGKASAN EVALUASI
        # ------------------------------------------------------
        ringkasan = workbook.active
        ringkasan.title = "Ringkasan Evaluasi"

        ringkasan.merge_cells("A1:D1")
        ringkasan["A1"] = "LAPORAN HASIL PENGUJIAN ARTIFICIAL NEURAL NETWORK"
        ringkasan["A1"].font = Font(
            bold=True, color="FFFFFF", size=14
        )
        ringkasan["A1"].fill = PatternFill(
            "solid", fgColor="166534"
        )
        ringkasan["A1"].alignment = Alignment(
            horizontal="center", vertical="center"
        )
        ringkasan.row_dimensions[1].height = 26

        ringkasan.merge_cells("A2:D2")
        ringkasan["A2"] = (
            "Prediksi Barang Keluar - Sistem Informasi Persediaan "
            "Toko Grosir Cucu"
        )
        ringkasan["A2"].alignment = Alignment(
            horizontal="center"
        )

        info = [
            ("Metode", "Artificial Neural Network / MLPRegressor"),
            ("Target", "Barang Keluar pada tanggal target"),
            ("Arsitektur", "7 Input -> 32 Neuron -> 16 Neuron -> 1 Output"),
            ("Aktivasi", "ReLU"),
            ("Optimizer / Solver", "Adam"),
            ("Normalisasi", "StandardScaler untuk input X dan target Y"),
            ("Pembagian Data", "Kronologis 80% Training dan 20% Testing"),
            (
                "Periode Histori",
                f"{sumber['tanggal_awal'].strftime('%d/%m/%Y')} "
                f"s.d. {sumber['tanggal_akhir'].strftime('%d/%m/%Y')}"
            ),
            (
                "Batas Data Testing",
                tanggal_batas.strftime("%d/%m/%Y")
            ),
            ("Total Dataset ANN", int(bundle.get("jumlah_data") or 0)),
            ("Data Training", int(bundle.get("jumlah_train") or 0)),
            ("Data Testing", int(bundle.get("jumlah_test") or 0)),
            ("MAE", float(bundle.get("mae") or 0)),
            ("RMSE", float(bundle.get("rmse") or 0)),
            (
                "R²",
                float(bundle["r2"])
                if bundle.get("r2") is not None else "-"
            ),
            (
                "Waktu Training",
                bundle.get("trained_at").strftime("%d/%m/%Y %H:%M:%S")
                if bundle.get("trained_at") else "-"
            )
        ]

        border_tipis = Border(
            left=Side(style="thin", color="D1D5DB"),
            right=Side(style="thin", color="D1D5DB"),
            top=Side(style="thin", color="D1D5DB"),
            bottom=Side(style="thin", color="D1D5DB")
        )

        for row_idx, (label, value) in enumerate(info, start=4):
            c1 = ringkasan.cell(row_idx, 1, label)
            c2 = ringkasan.cell(row_idx, 2, value)

            c1.font = Font(bold=True)
            c1.fill = PatternFill("solid", fgColor="F0FDF4")

            for cell in (c1, c2):
                cell.border = border_tipis
                cell.alignment = Alignment(
                    vertical="top", wrap_text=True
                )

        ringkasan["A22"] = "Fitur ANN"
        ringkasan["A22"].font = Font(
            bold=True, color="FFFFFF"
        )
        ringkasan["A22"].fill = PatternFill(
            "solid", fgColor="15803D"
        )

        fitur_info = [
            ("X1", "Barang Keluar Lag-1"),
            ("X2", "Barang Keluar Lag-2"),
            ("X3", "Barang Keluar Lag-3"),
            ("X4", "Barang Keluar Lag-7"),
            ("X5", "Rata-rata Barang Keluar 3 Hari"),
            ("X6", "Rata-rata Barang Keluar 7 Hari"),
            ("X7", "Hari dalam Minggu (Senin=0 s.d. Minggu=6)")
        ]

        for row_idx, (kode, nama) in enumerate(
            fitur_info, start=23
        ):
            ringkasan.cell(row_idx, 1, kode)
            ringkasan.cell(row_idx, 2, nama)
            for col in range(1, 3):
                ringkasan.cell(row_idx, col).border = border_tipis

        ringkasan.column_dimensions["A"].width = 28
        ringkasan.column_dimensions["B"].width = 58
        ringkasan.column_dimensions["C"].width = 16
        ringkasan.column_dimensions["D"].width = 16

        # ------------------------------------------------------
        # SHEET 2 - SELURUH HASIL TESTING
        # ------------------------------------------------------
        testing = workbook.create_sheet("Hasil Testing")

        headers = [
            "No",
            "Tanggal Target",
            "Kode Barang",
            "Nama Barang",
            "Satuan",
            "Aktual (Y)",
            "Prediksi ANN (Y-hat)",
            "Absolute Error"
        ]

        testing.merge_cells(
            start_row=1,
            start_column=1,
            end_row=1,
            end_column=len(headers)
        )
        testing.cell(1, 1).value = (
            "PERBANDINGAN DATA AKTUAL DAN PREDIKSI ANN - DATA TESTING"
        )
        testing.cell(1, 1).font = Font(
            bold=True, color="FFFFFF", size=14
        )
        testing.cell(1, 1).fill = PatternFill(
            "solid", fgColor="166534"
        )
        testing.cell(1, 1).alignment = Alignment(
            horizontal="center"
        )

        for col_idx, header in enumerate(headers, start=1):
            cell = testing.cell(3, col_idx, header)
            cell.font = Font(bold=True, color="FFFFFF")
            cell.fill = PatternFill(
                "solid", fgColor="15803D"
            )
            cell.alignment = Alignment(
                horizontal="center",
                vertical="center",
                wrap_text=True
            )
            cell.border = border_tipis

        for row_idx, item in enumerate(
            hasil_testing, start=4
        ):
            values = [
                row_idx - 3,
                item["tanggal"],
                item["kode_barang"],
                item["nama_barang"],
                item["satuan_kecil"],
                item["aktual"],
                item["prediksi"],
                item["absolute_error"]
            ]

            for col_idx, value in enumerate(values, start=1):
                cell = testing.cell(row_idx, col_idx, value)
                cell.border = border_tipis
                cell.alignment = Alignment(
                    vertical="center"
                )

            testing.cell(row_idx, 2).number_format = "dd/mm/yyyy"
            testing.cell(row_idx, 6).number_format = "0.0000"
            testing.cell(row_idx, 7).number_format = "0.0000"
            testing.cell(row_idx, 8).number_format = "0.0000"

        widths = {
            "A": 8,
            "B": 17,
            "C": 16,
            "D": 30,
            "E": 15,
            "F": 14,
            "G": 20,
            "H": 18
        }

        for col, width in widths.items():
            testing.column_dimensions[col].width = width

        testing.freeze_panes = "A4"
        testing.auto_filter.ref = (
            f"A3:H{3 + len(hasil_testing)}"
        )

        output = io.BytesIO()
        workbook.save(output)
        output.seek(0)

        nama_output = (
            "Laporan_Evaluasi_ANN_Toko_Grosir_Cucu_"
            f"{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
        )

        return send_file(
            output,
            as_attachment=True,
            download_name=nama_output,
            mimetype=(
                "application/"
                "vnd.openxmlformats-officedocument."
                "spreadsheetml.sheet"
            )
        )

    except Exception as e:
        print("ERROR EXPORT LAPORAN ANN:", e)

        flash(
            f"Laporan ANN gagal diexport: {e}",
            "danger"
        )

        return redirect("/admin/laporan-ann")


# ==========================================================
# HALAMAN PREDIKSI ANN
# ==========================================================
@app.route("/admin/prediksi-ann")
def prediksi_ann():

    if not cek_akses_admin_ann():
        return redirect("/login")

    cursor = mysql.connection.cursor()

    try:
        # Rentang memakai gabungan histori masuk + keluar.
        cursor.execute(
            """
            SELECT
                LEAST(
                    COALESCE(
                        (SELECT MIN(DATE(tanggal)) FROM transaksi_masuk),
                        '9999-12-31'
                    ),
                    COALESCE(
                        (SELECT MIN(DATE(tanggal)) FROM transaksi_keluar),
                        '9999-12-31'
                    )
                ),
                GREATEST(
                    COALESCE(
                        (SELECT MAX(DATE(tanggal)) FROM transaksi_masuk),
                        '1000-01-01'
                    ),
                    COALESCE(
                        (SELECT MAX(DATE(tanggal)) FROM transaksi_keluar),
                        '1000-01-01'
                    )
                )
            """
        )

        rentang = cursor.fetchone()
        tanggal_awal = _normalisasi_tanggal(rentang[0])
        tanggal_akhir = _normalisasi_tanggal(rentang[1])

        cursor.execute("SELECT COUNT(*) FROM barang")
        jumlah_barang = cursor.fetchone()[0]

        cursor.execute("SELECT COUNT(*) FROM transaksi_masuk")
        jumlah_transaksi_masuk = cursor.fetchone()[0]

        cursor.execute("SELECT COUNT(*) FROM transaksi_keluar")
        jumlah_transaksi_keluar = cursor.fetchone()[0]

        cursor.close()

        return render_template(
            "admin/prediksi_ann.html",
            tanggal_awal=tanggal_awal,
            tanggal_akhir=tanggal_akhir,
            jumlah_barang=jumlah_barang,
            jumlah_transaksi_masuk=jumlah_transaksi_masuk,
            jumlah_transaksi_keluar=jumlah_transaksi_keluar,
            model_info=_status_model_ann()
        )

    except Exception as e:
        try:
            cursor.close()
        except Exception:
            pass

        print("ERROR HALAMAN PREDIKSI ANN:", e)

        flash(
            f"Halaman prediksi gagal dibuka: {e}",
            "danger"
        )

        return redirect("/admin/dataset-ann")


# ==========================================================
# TRAINING MODEL ANN STOK
# ==========================================================
@app.route(
    "/admin/prediksi-ann/training",
    methods=["POST"]
)
def training_ann_stok():

    if not cek_akses_admin_ann():
        return redirect("/login")

    try:
        X, y, tanggal_target, sumber = (
            _bentuk_data_training_ann()
        )

        # ------------------------------------------------------
        # Split kronologis 80% train - 20% test.
        # Tidak memakai random split karena ini data waktu.
        # ------------------------------------------------------
        tanggal_unik = sorted(set(tanggal_target))

        if len(tanggal_unik) < 10:
            raise ValueError(
                "Jumlah hari efektif untuk training terlalu sedikit."
            )

        index_batas = max(
            1,
            int(len(tanggal_unik) * 0.80)
        )

        if index_batas >= len(tanggal_unik):
            index_batas = len(tanggal_unik) - 1

        tanggal_batas = tanggal_unik[index_batas]

        mask_train = np.asarray([
            t < tanggal_batas
            for t in tanggal_target
        ])

        mask_test = ~mask_train

        X_train = X[mask_train]
        y_train = y[mask_train]
        X_test = X[mask_test]
        y_test = y[mask_test]

        if (
            len(X_train) == 0
            or len(X_test) == 0
        ):
            raise ValueError(
                "Pembagian data training/testing gagal."
            )

        # ------------------------------------------------------
        # Scaling
        # ------------------------------------------------------
        scaler_x = StandardScaler()
        scaler_y = StandardScaler()

        X_train_scaled = scaler_x.fit_transform(X_train)
        X_test_scaled = scaler_x.transform(X_test)

        y_train_scaled = scaler_y.fit_transform(
            y_train.reshape(-1, 1)
        ).ravel()

        # ------------------------------------------------------
        # ANN / Multilayer Perceptron
        # ------------------------------------------------------
        model = MLPRegressor(
            hidden_layer_sizes=(32, 16),
            activation="relu",
            solver="adam",
            learning_rate_init=0.001,
            max_iter=2000,
            early_stopping=True,
            validation_fraction=0.15,
            n_iter_no_change=50,
            random_state=42
        )

        model.fit(
            X_train_scaled,
            y_train_scaled
        )

        pred_scaled = model.predict(
            X_test_scaled
        )

        prediksi = scaler_y.inverse_transform(
            pred_scaled.reshape(-1, 1)
        ).ravel()

        prediksi = np.maximum(
            prediksi,
            0
        )

        mae = float(
            mean_absolute_error(
                y_test,
                prediksi
            )
        )

        rmse = float(
            np.sqrt(
                mean_squared_error(
                    y_test,
                    prediksi
                )
            )
        )

        try:
            r2 = float(
                r2_score(
                    y_test,
                    prediksi
                )
            )
        except Exception:
            r2 = None

        os.makedirs(
            os.path.dirname(MODEL_ANN_PATH),
            exist_ok=True
        )

        bundle = {
            "model": model,
            "scaler_x": scaler_x,
            "scaler_y": scaler_y,
            "fitur": FITUR_ANN_STOK,
            "target": "barang_keluar",
            "jenis_model": "ANN Prediksi Kebutuhan Barang",
            "trained_at": datetime.now(),
            "mae": mae,
            "rmse": rmse,
            "r2": r2,
            "jumlah_data": int(len(X)),
            "jumlah_train": int(len(X_train)),
            "jumlah_test": int(len(X_test)),
            "tanggal_awal": sumber[
                "tanggal_awal"
            ],
            "tanggal_akhir": sumber[
                "tanggal_akhir"
            ]
        }

        joblib.dump(
            bundle,
            MODEL_ANN_PATH
        )

        flash(
            "Training ANN Prediksi Kebutuhan berhasil. "
            f"Data: {len(X)} record | "
            f"MAE: {mae:.2f} | "
            f"RMSE: {rmse:.2f}",
            "success"
        )

        return redirect(
            "/admin/prediksi-ann"
        )

    except Exception as e:
        print("ERROR TRAINING ANN:", e)

        flash(
            f"Training ANN gagal: {e}",
            "danger"
        )

        return redirect(
            "/admin/prediksi-ann"
        )

# ==========================================================
# PREDIKSI STOK BEBERAPA HARI KE DEPAN
# ==========================================================
def _prediksi_stok_semua_barang(jumlah_hari):
    """
    Prediksi kebutuhan seluruh barang untuk 3/7/14/30 hari.

    ANN memprediksi barang keluar harian secara rekursif.
    Proyeksi stok = stok aktual - akumulasi prediksi kebutuhan.

    Barang masuk masa depan tidak ditambahkan sebelum benar-benar
    tercatat sebagai transaksi Gudang.
    """

    if jumlah_hari not in (3, 7, 14, 30):
        raise ValueError(
            "Periode prediksi hanya 3, 7, 14, atau 30 hari."
        )

    if not os.path.exists(MODEL_ANN_PATH):
        raise ValueError(
            "Model ANN Prediksi Kebutuhan belum tersedia. "
            "Lakukan Training ANN terlebih dahulu."
        )

    bundle = joblib.load(MODEL_ANN_PATH)
    model = bundle["model"]
    scaler_x = bundle["scaler_x"]
    scaler_y = bundle["scaler_y"]

    sumber = _ambil_dataset_stok_harian()
    hasil_prediksi = []

    tanggal_mulai_prediksi = (
        datetime.now().date() + timedelta(days=1)
    )

    for master in sumber["master_barang"]:
        barang_id = int(master[0])
        kode_barang = master[1]
        nama_barang = master[2]
        satuan_kecil = master[3]
        stok_sekarang = max(0, int(master[4] or 0))

        histori = sumber["dataset_per_barang"].get(
            barang_id,
            []
        )

        if len(histori) < 7:
            continue

        # Tujuh nilai kebutuhan terakhir, lama -> baru.
        keluar_hist = [
            float(row["barang_keluar"])
            for row in histori[-7:]
        ]

        stok_proyeksi = float(stok_sekarang)
        total_prediksi_kebutuhan = 0.0
        detail_harian = []
        tanggal_habis = None

        for langkah in range(1, jumlah_hari + 1):
            tanggal_prediksi = (
                tanggal_mulai_prediksi
                + timedelta(days=langkah - 1)
            )

            fitur = np.asarray([[
                keluar_hist[-1],
                keluar_hist[-2],
                keluar_hist[-3],
                keluar_hist[-7],
                float(np.mean(keluar_hist[-3:])),
                float(np.mean(keluar_hist[-7:])),
                tanggal_prediksi.weekday()
            ]], dtype=float)

            fitur_scaled = scaler_x.transform(fitur)
            pred_scaled = model.predict(fitur_scaled)

            prediksi_kebutuhan = (
                scaler_y.inverse_transform(
                    np.asarray(pred_scaled).reshape(-1, 1)
                ).ravel()[0]
            )

            # Kebutuhan tidak boleh negatif.
            prediksi_kebutuhan = max(
                0.0,
                float(prediksi_kebutuhan)
            )

            total_prediksi_kebutuhan += prediksi_kebutuhan

            # Tidak ada asumsi restock masa depan.
            stok_proyeksi = max(
                0.0,
                stok_proyeksi - prediksi_kebutuhan
            )

            if (
                tanggal_habis is None
                and stok_proyeksi < 0.5
            ):
                tanggal_habis = tanggal_prediksi

            detail_harian.append({
                "hari_ke": langkah,
                "tanggal": tanggal_prediksi,
                "prediksi_kebutuhan": int(
                    round(prediksi_kebutuhan)
                ),
                "stok_prediksi": int(
                    round(stok_proyeksi)
                )
            })

            # Prediksi hari ini menjadi lag untuk hari berikutnya.
            keluar_hist.append(prediksi_kebutuhan)
            keluar_hist = keluar_hist[-7:]

        stok_akhir = int(round(stok_proyeksi))
        total_kebutuhan = int(
            round(total_prediksi_kebutuhan)
        )

        if stok_akhir <= 0:
            status = "Berpotensi Habis"
            status_class = "danger"
            rekomendasi = (
                "Segera lakukan pemesanan ke supplier."
            )

        elif (
            stok_sekarang > 0
            and stok_akhir
            <= max(5, stok_sekarang * 0.25)
        ):
            status = "Menipis"
            status_class = "warning"
            rekomendasi = (
                "Disarankan melakukan pemesanan ke supplier."
            )

        else:
            status = "Aman"
            status_class = "success"
            rekomendasi = (
                "Stok masih mencukupi untuk periode prediksi."
            )

        hasil_prediksi.append({
            "barang_id": barang_id,
            "kode_barang": kode_barang,
            "nama_barang": nama_barang,
            "satuan_kecil": satuan_kecil,
            "stok_sekarang": stok_sekarang,

            # Key lama dipertahankan agar template Admin
            # yang sudah berhasil tetap kompatibel.
            "stok_prediksi": stok_akhir,

            "prediksi_kebutuhan": total_kebutuhan,
            "status": status,
            "status_class": status_class,
            "tanggal_habis": tanggal_habis,
            "rekomendasi": rekomendasi,
            "detail_harian": detail_harian
        })

    urutan_status = {
        "Berpotensi Habis": 0,
        "Menipis": 1,
        "Aman": 2
    }

    hasil_prediksi.sort(
        key=lambda x: (
            urutan_status.get(x["status"], 9),
            x["stok_prediksi"],
            x["nama_barang"]
        )
    )

    return hasil_prediksi


@app.route("/admin/prediksi-ann/proses", methods=["POST"])
def proses_prediksi_ann():
    if not cek_akses_admin_ann():
        return redirect("/login")

    try:
        jumlah_hari = int(request.form.get("jumlah_hari", "7"))
        hasil = _prediksi_stok_semua_barang(jumlah_hari)

        # ======================================================
        # SIMPAN HASIL PREDIKSI KE DATABASE
        # ======================================================
        # Satu periode hanya menyimpan hasil prediksi terbaru.
        # Hasil lama untuk horizon yang sama diganti agar halaman
        # publik nantinya selalu membaca prediksi terbaru.
        cursor = mysql.connection.cursor()

        try:
            tanggal_prediksi_db = datetime.now().date()

            cursor.execute(
                "DELETE FROM hasil_prediksi WHERE periode_hari = %s",
                (jumlah_hari,)
            )

            query_simpan_prediksi = """
                INSERT INTO hasil_prediksi (
                    barang_id,
                    tanggal_prediksi,
                    periode_hari,
                    prediksi_kebutuhan,
                    stok_saat_prediksi,
                    rekomendasi_stok,
                    tanggal_habis
                )
                VALUES (%s, %s, %s, %s, %s, %s, %s)
            """

            for item in hasil:
                cursor.execute(
                    query_simpan_prediksi,
                    (
                        item["barang_id"],
                        tanggal_prediksi_db,
                        jumlah_hari,
                        item["prediksi_kebutuhan"],
                        item["stok_sekarang"],
                        item["stok_prediksi"],
                        item["tanggal_habis"]
                    )
                )

            mysql.connection.commit()

        except Exception:
            mysql.connection.rollback()
            cursor.close()
            raise

        cursor.execute("SELECT COUNT(*) FROM barang")
        jumlah_barang = cursor.fetchone()[0]
        cursor.execute("SELECT COUNT(*) FROM transaksi_masuk")
        jumlah_transaksi_masuk = cursor.fetchone()[0]
        cursor.execute("SELECT COUNT(*) FROM transaksi_keluar")
        jumlah_transaksi_keluar = cursor.fetchone()[0]
        cursor.close()

        sumber = _ambil_dataset_stok_harian()
        ringkasan = {
            "aman": sum(1 for x in hasil if x["status"] == "Aman"),
            "menipis": sum(1 for x in hasil if x["status"] == "Menipis"),
            "habis": sum(1 for x in hasil if x["status"] == "Berpotensi Habis")
        }

        return render_template(
            "admin/prediksi_ann.html",
            tanggal_awal=sumber["tanggal_awal"],
            tanggal_akhir=sumber["tanggal_akhir"],
            jumlah_barang=jumlah_barang,
            jumlah_transaksi_masuk=jumlah_transaksi_masuk,
            jumlah_transaksi_keluar=jumlah_transaksi_keluar,
            model_info=_status_model_ann(),
            hasil_prediksi=hasil,
            jumlah_hari=jumlah_hari,
            ringkasan_prediksi=ringkasan,
            tanggal_prediksi=datetime.now()
        )

    except Exception as e:
        print("ERROR PROSES PREDIKSI ANN:", e)
        flash(f"Prediksi ANN gagal: {e}", "danger")
        return redirect("/admin/prediksi-ann")
