<?php
/**
 * Cart Totals Calculator.
 *
 * Pure calculation class — no DB writes.
 * Accepts a raw cart + items array and returns a fully
 * computed totals object ready for display and order creation.
 *
 * Calculation order:
 *   1. Line item subtotals (qty × unit_price + option modifiers)
 *   2. Apply option price modifiers per item
 *   3. Cart subtotal = sum of line item totals
 *   4. Coupon / promotion discounts
 *   5. Shipping cost (passed in — calculated by Shipping module)
 *   6. Tax (calculated on post-discount subtotal + shipping if applicable)
 *   7. Payment surcharge (added last — not discountable)
 *   8. Grand total
 *
 * @package TitanCart\Modules\Cart
 */

namespace TitanCart\Modules\Cart;

defined( 'ABSPATH' ) || exit;

class CartTotals {

    /**
     * Compute all cart totals from a hydrated cart array.
     *
     * @param array $cart  Cart row from DB.
     * @param array $items Items from CartRepository::get_items(), each hydrated
     *                     with product data (price, options, etc.).
     * @param array $config {
     *   coupon_discount   float  Flat discount amount from active coupon.
     *   shipping_cost     float  Resolved shipping cost (0 if not yet calculated).
     *   tax_total         float  Pre-calculated tax total (0 if not yet calculated).
     *   payment_surcharge float  Payment method surcharge.
     *   tip_total         float  Customer tip / donation, server-computed (added after tax).
     *   currency          string 3-letter currency code.
     * }
     * @return array<string,mixed> Totals object.
     */
    public function calculate( array $cart, array $items, array $config = [] ): array {
        $config = array_merge( [
            'coupon_discount'   => 0.0,
            'shipping_cost'     => 0.0,
            'tax_total'         => 0.0,
            'payment_surcharge' => 0.0,
            'tip_total'         => 0.0,
            'currency'          => $cart['currency'] ?? 'USD',
        ], $config );

        // ── 1. Line item totals ───────────────────────────────────────────────
        $line_items    = [];
        $raw_subtotal  = 0.0;
        $total_qty     = 0;

        foreach ( $items as $item ) {
            $line = $this->calculate_line_item( $item );
            $line_items[]  = $line;
            $raw_subtotal += $line['line_total'];
            $total_qty    += (int) $item['quantity'];
        }

        // Allow hook: titancart_cart_line_items
        do_action_ref_array( 'titancart_cart_line_items', [ &$line_items, &$raw_subtotal, $cart ] );

        // ── 2. Coupon / promotion discount ────────────────────────────────────
        $coupon_discount = (float) $config['coupon_discount'];
        // Clamp discount so it never exceeds the subtotal.
        $coupon_discount = min( $coupon_discount, $raw_subtotal );

        $discounted_subtotal = $raw_subtotal - $coupon_discount;

        // ── 3. Shipping ───────────────────────────────────────────────────────
        $shipping_cost = (float) $config['shipping_cost'];

        // ── 4. Tax ────────────────────────────────────────────────────────────
        $tax_total = (float) $config['tax_total'];

        // ── 5. Payment surcharge ──────────────────────────────────────────────
        $payment_surcharge = (float) $config['payment_surcharge'];

        // ── 6. Tip / Donation ─────────────────────────────────────────────────
        // Server-authoritative amount computed by CheckoutManager::resolve_tip()
        // (round-up / preset / clamped custom). Added last, after tax — never
        // taxed, never discounted, mirroring the surcharge slot. 0.0 unless the
        // paid Tip/Donation extension supplied a tip via titancart_tip_config.
        $tip_total = (float) $config['tip_total'];

        // ── 7. Grand total ────────────────────────────────────────────────────
        $grand_total = $discounted_subtotal
            + $shipping_cost
            + $tax_total
            + $payment_surcharge
            + $tip_total;

        $grand_total = max( 0.0, $grand_total ); // Never negative.

        $totals = [
            'currency'          => $config['currency'],
            'item_count'        => $total_qty,
            'line_count'        => count( $line_items ),
            'subtotal'          => round( $raw_subtotal, 2 ),
            'coupon_discount'   => round( $coupon_discount, 2 ),
            'discounted_subtotal' => round( $discounted_subtotal, 2 ),
            'shipping_cost'     => round( $shipping_cost, 2 ),
            'tax_total'         => round( $tax_total, 2 ),
            'payment_surcharge' => round( $payment_surcharge, 2 ),
            'tip_total'         => round( $tip_total, 2 ),
            'total'             => round( $grand_total, 2 ),
            'line_items'        => $line_items,
        ];

        // Allow post-calculation modifications.
        do_action_ref_array( 'titancart_cart_totals', [ &$totals, $cart, $items ] );

        return $totals;
    }

    // ── Line item calculation ─────────────────────────────────────────────────

    /**
     * Calculate totals for a single line item.
     *
     * Handles option price modifiers (absolute and percent).
     *
     * @param  array $item  Hydrated item row.
     * @return array
     */
    public function calculate_line_item( array $item ): array {
        $quantity       = (int) $item['quantity'];
        $unit_price     = (float) $item['unit_price'];
        $options        = [];
        $modifier_total = 0.0;

        if ( ! empty( $item['options'] ) ) {
            $options = is_string( $item['options'] )
                ? json_decode( $item['options'], true )
                : $item['options'];
            $options = is_array( $options ) ? $options : [];
        }

        // S141 — running-total math for Sequential Options compound pricing.
        //
        // Iterate options in their stored order. CartManager::validate_options
        // (S141) emits entries in topo order (parents before dependents,
        // matching the PDP visual flow) and stamps a `compound_pricing` flag
        // onto each entry. For each option:
        //   - Absolute modifiers always stack additively (no semantic change).
        //   - Percent modifiers when `compound_pricing=1` apply to the running
        //     effective price (compound — the option's % stacks on top of every
        //     prior modifier).
        //   - Percent modifiers when `compound_pricing=0` apply to the base
        //     `unit_price` (additive — the historical behavior, preserved for
        //     every existing product and every existing cart entry).
        //
        // Back-compat: old cart entries from before S141 have no
        // `compound_pricing` key. `! empty($opt['compound_pricing'])` returns
        // false for both unset and 0, so `$is_compound = false`, and the math
        // collapses to the original additive form. Zero regression.
        $running_price = $unit_price;
        foreach ( $options as $opt ) {
            $modifier = (float) ( $opt['modifier'] ?? 0 );
            if ( $modifier === 0.0 ) {
                continue;
            }
            $modifier_type = $opt['modifier_type'] ?? 'A';
            $is_compound   = ! empty( $opt['compound_pricing'] );

            if ( $modifier_type === 'P' ) {
                $base           = $is_compound ? $running_price : $unit_price;
                $running_price += $base * ( $modifier / 100 );
            } else {
                $running_price += $modifier;
            }
        }

        $modifier_total  = $running_price - $unit_price;
        $effective_price = max( 0.0, $running_price );
        $line_total      = round( $effective_price * $quantity, 4 );

        return [
            'item_id'         => $item['id'] ?? null,
            'product_id'      => (int) $item['product_id'],
            'variation_id'    => ! empty( $item['variation_id'] ) ? (int) $item['variation_id'] : null,
            'quantity'        => $quantity,
            'unit_price'      => round( $unit_price, 4 ),
            'modifier_total'  => round( $modifier_total, 4 ),
            'effective_price' => round( $effective_price, 4 ),
            'line_total'      => $line_total,
            'options'         => $options,
            // Product display fields (attached by CartManager).
            'name'            => $item['name']      ?? '',
            'sku'             => $item['sku']        ?? '',
            'slug'            => $item['slug']       ?? '',
            'image'           => $item['image']      ?? null,
            'stock_status'    => $item['stock_status'] ?? 'in_stock',
            'max_quantity'    => $item['max_quantity'] ?? null,
            'meta'            => $item['meta']         ?? null,
            // ⚠️ Shipping weight in POUNDS (KI-93). This projection is a WHITELIST,
            // and CartManager::get_cart() returns `line_items` AS `$cart['items']`
            // — so a key that CartManager attaches but this array omits is silently
            // DROPPED before any consumer sees it (which is what happened to
            // min_quantity and quantity_step, and to weight on the first cut of
            // this fix). ShippingManager::build_cart_totals() reads $item['weight']
            // off exactly this array, so the weight windows and by_weight bands
            // depend on this line existing.
            'weight'          => (float) ( $item['weight'] ?? 0 ),
        ];
    }

    // ── Formatting helpers ────────────────────────────────────────────────────

    /**
     * Format a money amount using the store's currency settings.
     *
     * @param  float  $amount
     * @param  string $symbol
     * @param  bool   $symbol_after
     * @param  int    $decimals
     * @return string
     */
    public static function format_money(
        float  $amount,
        string $symbol       = '$',
        bool   $symbol_after = false,
        int    $decimals     = 2
    ): string {
        $formatted = number_format( $amount, $decimals );
        return $symbol_after
            ? $formatted . $symbol
            : $symbol . $formatted;
    }
}
