package de.gecheckt.pdf.umbenenner.application.cost;

import java.math.BigDecimal;
import java.math.BigInteger;

/**
 * Interpretiert aggregierte Token-Rohkosten in {@link BigDecimal}-USD und
 * bestimmt die Status-Flags des {@link CostResult}.
 *
 * <p>Der CostCalculator fuehrt selbst <strong>keine</strong> Multiplikation
 * Tokens&times;Preis durch. Multiplikation findet im SQL-Adapter statt
 * (Pro-Attempt-Snapshot); der CostCalculator interpretiert nur die bereits
 * aufsummierten Roh-Werte und uebersetzt sie in einen anzeigetauglichen
 * Betrag.
 *
 * <p>Es findet <strong>keine</strong> interne Rundung statt; Rundung
 * auf vier Nachkommastellen erfolgt ausschlie&szlig;lich im GUI-Layer
 * (CostFormatter).
 */
public final class CostCalculator {

    /** Divisor fuer die Umrechnung Nano-USD &rarr; USD. */
    private static final BigDecimal NANO_USD_PER_USD = new BigDecimal("1000000000");

    /**
     * Erzeugt eine Instanz. Stateless; Wiederverwendung als Singleton ist erlaubt.
     */
    public CostCalculator() {
    }

    /**
     * Interpretation aggregierter Long-Werte einer Tabellenzeile.
     *
     * <p>Verwendet {@code long}-Pro-Group-Aggregation aus SQL.
     *
     * @param sumInputCostNanoUsd     Summe der Input-Kosten in Nano-USD; {@code null} wenn keiner
     * @param sumOutputCostNanoUsd    Summe der Output-Kosten in Nano-USD; {@code null} wenn keiner
     * @param hasPartialTokenData     mindestens ein Token-Feld fehlte
     * @param hasMissingPriceSnapshot mindestens ein Preis-Snapshot fehlte
     * @param hasCacheTokensIgnored   Cache-Tokens kamen vor
     * @param hasAnyTokenData         es lagen ueberhaupt Token-Daten vor
     * @return interpretiertes Kosten-Ergebnis
     */
    public CostResult formatRow(
            Long sumInputCostNanoUsd,
            Long sumOutputCostNanoUsd,
            boolean hasPartialTokenData,
            boolean hasMissingPriceSnapshot,
            boolean hasCacheTokensIgnored,
            boolean hasAnyTokenData) {
        BigInteger sumInput = sumInputCostNanoUsd == null ? null : BigInteger.valueOf(sumInputCostNanoUsd);
        BigInteger sumOutput = sumOutputCostNanoUsd == null ? null : BigInteger.valueOf(sumOutputCostNanoUsd);
        boolean hasAnyInputCost = sumInput != null;
        boolean hasAnyOutputCost = sumOutput != null;
        return interpret(sumInput, sumOutput, hasAnyInputCost, hasAnyOutputCost,
                hasAnyTokenData, hasPartialTokenData, hasMissingPriceSnapshot, hasCacheTokensIgnored);
    }

    /**
     * Interpretation aufsummierter BigInteger-Aggregate fuer Kopfzeilen und
     * Run-Summary-Banner.
     *
     * <p>Wird in V3.3 in einem spaeteren Arbeitspaket vollstaendig implementiert
     * (AP-B). Fuer AP-A genuegt diese Stub-Methode, die mit
     * {@link UnsupportedOperationException} fehlt, sobald sie aufgerufen wird.
     *
     * @param totalInputCostNanoUsd  aufaddierte Input-Kosten in Nano-USD oder {@code null}
     * @param totalOutputCostNanoUsd aufaddierte Output-Kosten in Nano-USD oder {@code null}
     * @param hasAnyInputCost        Flag, ob ueberhaupt ein berechenbarer Input-Kostenanteil vorlag
     * @param hasAnyOutputCost       Flag, ob ueberhaupt ein berechenbarer Output-Kostenanteil vorlag
     * @param hasAnyTokenData        Flag, ob ueberhaupt Token-Daten vorlagen
     * @param hasPartialTokenData    Flag, ob mindestens ein Token-Feld fehlte
     * @param hasMissingPriceSnapshot Flag, ob mindestens ein Preis-Snapshot fehlte
     * @param hasCacheTokensIgnored  Flag, ob Cache-Tokens vorkamen
     * @return interpretiertes Kosten-Ergebnis
     */
    public CostResult formatTotal(
            BigInteger totalInputCostNanoUsd,
            BigInteger totalOutputCostNanoUsd,
            boolean hasAnyInputCost,
            boolean hasAnyOutputCost,
            boolean hasAnyTokenData,
            boolean hasPartialTokenData,
            boolean hasMissingPriceSnapshot,
            boolean hasCacheTokensIgnored) {
        throw new UnsupportedOperationException("formatTotal wird in einem spaeteren Arbeitspaket implementiert");
    }

    /**
     * Interpretation eines einzelnen Versuchs (z.B. fuer den History-Tab).
     *
     * <p>Multipliziert Tokens&times;Preise lokal, da hier keine Aggregation
     * vorliegt. Cache-Tokens werden nur ueber das {@code hasCacheTokens}-Flag
     * markiert; in der Berechnung selbst werden sie nicht beruecksichtigt.
     *
     * @param inputTokens                  Anzahl Input-Tokens; {@code null} moeglich
     * @param outputTokens                 Anzahl Output-Tokens; {@code null} moeglich
     * @param priceInputPerTokenNanoUsd    Snapshot-Preis Input; {@code null} moeglich
     * @param priceOutputPerTokenNanoUsd   Snapshot-Preis Output; {@code null} moeglich
     * @param hasCacheTokens               Cache-Tokens lagen im Versuch vor
     * @return interpretiertes Kosten-Ergebnis
     */
    public CostResult calculateAttempt(
            Long inputTokens,
            Long outputTokens,
            Long priceInputPerTokenNanoUsd,
            Long priceOutputPerTokenNanoUsd,
            boolean hasCacheTokens) {

        boolean hasAnyTokenData = inputTokens != null || outputTokens != null;

        BigInteger inputCost = null;
        BigInteger outputCost = null;
        boolean partialTokens = false;
        boolean missingPriceSnapshot = false;

        if (inputTokens != null) {
            if (priceInputPerTokenNanoUsd != null) {
                inputCost = BigInteger.valueOf(inputTokens)
                        .multiply(BigInteger.valueOf(priceInputPerTokenNanoUsd));
            } else {
                missingPriceSnapshot = true;
            }
        } else if (outputTokens != null) {
            partialTokens = true;
        }

        if (outputTokens != null) {
            if (priceOutputPerTokenNanoUsd != null) {
                outputCost = BigInteger.valueOf(outputTokens)
                        .multiply(BigInteger.valueOf(priceOutputPerTokenNanoUsd));
            } else {
                missingPriceSnapshot = true;
            }
        } else if (inputTokens != null) {
            partialTokens = true;
        }

        boolean hasAnyInputCost = inputCost != null;
        boolean hasAnyOutputCost = outputCost != null;

        return interpret(inputCost, outputCost, hasAnyInputCost, hasAnyOutputCost,
                hasAnyTokenData, partialTokens, missingPriceSnapshot, hasCacheTokens);
    }

    /**
     * Gemeinsame Interpretationslogik fuer Row- und Attempt-Pfad.
     *
     * @return berechnetes {@link CostResult}
     */
    private CostResult interpret(
            BigInteger inputCostNano,
            BigInteger outputCostNano,
            boolean hasAnyInputCost,
            boolean hasAnyOutputCost,
            boolean hasAnyTokenData,
            boolean partialTokens,
            boolean missingPriceSnapshot,
            boolean cacheTokensIgnored) {

        boolean noTokens = !hasAnyTokenData;
        boolean hasAnyCalculatedCost = hasAnyInputCost || hasAnyOutputCost;
        BigDecimal amountUsd = null;
        if (hasAnyCalculatedCost) {
            BigInteger sum = (inputCostNano != null ? inputCostNano : BigInteger.ZERO)
                    .add(outputCostNano != null ? outputCostNano : BigInteger.ZERO);
            amountUsd = new BigDecimal(sum).divide(NANO_USD_PER_USD);
        }
        boolean exact = !noTokens
                && !partialTokens
                && !missingPriceSnapshot
                && !cacheTokensIgnored
                && hasAnyCalculatedCost;
        return new CostResult(
                amountUsd,
                exact,
                partialTokens,
                missingPriceSnapshot,
                noTokens,
                cacheTokensIgnored,
                hasAnyCalculatedCost);
    }
}
