<?php
/**
 * TaxManager — Main tax calculation service.
 *
 * Responsibilities:
 *   1. calculate_for_cart($cart, $address) — the primary entry point, called
 *      from the `titancart_checkout_tax_total` filter hook.
 *   2. get_rate_breakdown($cart, $address) — returns per-class breakdown for
 *      display (e.g. "VAT 20% — $12.50").
 *
 * Calculation algorithm:
 *
 *   Step 1 — Resolve the tax zone from the shipping address.
 *   Step 2 — Fetch every product's tax_class_id from the DB.
 *   Step 3 — For each unique class_id, load applicable rate rows.
 *   Step 4 — Group rate rows by priority (same priority = summed together).
 *   Step 5 — Within each priority group:
 *               a) Non-compound rates: apply to the original subtotal.
 *               b) Compound rates: apply to (subtotal + all previously calculated tax).
 *   Step 6 — If includes_tax = 1, back-calculate the embedded tax amount instead.
 *
 * Tax is applied per line-item and summed for accuracy (avoids rounding issues
 * if individual item prices differ). The cart `subtotal` field used here is the
 * total of (price × quantity) before any discount.
 *
 * @package TitanCart\Modules\Tax
 */

namespace TitanCart\Modules\Tax;

use TitanCart\DB\Database;
use TitanCart\Core\LogService;

defined( 'ABSPATH' ) || exit;

class TaxManager {

    /**
     * How long to suppress a retry after a FAILED default-seed attempt (seconds).
     *
     * KI-83: `maybe_seed_defaults()` runs on `titancart_loaded` — i.e. on every
     * request — so a failed seed must not retry immediately (that would re-attempt
     * an INSERT on every page load forever), nor be recorded as permanently done
     * (that is the defect: the store ends up with zero tax classes, silently,
     * forever). A short transient gives bounded retry.
     */
    private const SEED_RETRY_DELAY = 5 * MINUTE_IN_SECONDS;

    private Database $db;
    private TaxZone  $zone_repo;
    private TaxRate  $rate_repo;

    public function __construct() {
        $this->db        = titancart_db();
        $this->zone_repo = new TaxZone();
        $this->rate_repo = new TaxRate();
    }

    // ══════════════════════════════════════════════════════════════════════════
    // PRIMARY ENTRY POINT
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Return the total tax amount for the given cart and address.
     *
     * This is the value wired to the `titancart_checkout_tax_total` filter.
     *
     * @param  array $cart     Cart array (must include 'items' with 'product_id',
     *                         'quantity', and a per-unit price under one of
     *                         'effective_price' (post-options), 'unit_price', or
     *                         'price').
     * @param  array $address  Shipping address {country, state, postcode}.
     * @return float
     */
    public function calculate_for_cart( array $cart, array $address ): float {
        $breakdown = $this->get_rate_breakdown( $cart, $address );
        $total     = 0.0;

        foreach ( $breakdown as $entry ) {
            $total += $entry['tax_amount'];
        }

        return round( $total, 2 );
    }

    /**
     * Return a per-tax-class breakdown of amounts for display or storage.
     *
     * Each element:
     *   {class_id, class_name, rate_percent, tax_amount, includes_tax}
     *
     * @param  array $cart
     * @param  array $address
     * @return array[]
     */
    public function get_rate_breakdown( array $cart, array $address ): array {
        if ( empty( $cart['items'] ) ) {
            return [];
        }

        // ── Step 1: Zone resolution ───────────────────────────────────────────
        $zone_ids = $this->zone_repo->get_matching_zone_ids( $address );

        if ( empty( $zone_ids ) ) {
            return [];
        }

        // ── Step 2: Resolve product → tax_class_id mapping ───────────────────
        $product_ids = array_unique( array_column( $cart['items'], 'product_id' ) );

        if ( empty( $product_ids ) ) {
            return [];
        }

        // Fetch tax_class_id for each product directly from the DB.
        // Cart items don't carry tax_class_id, so we look it up here.
        $product_class_rows = $this->db->getArray(
            "SELECT id, tax_class_id
             FROM {$this->db->table('products')}
             WHERE id IN (?a)",
            $product_ids
        );

        // product_id → tax_class_id (null-safe)
        $product_class_map = [];
        foreach ( $product_class_rows as $row ) {
            if ( $row['tax_class_id'] ) {
                $product_class_map[ (int) $row['id'] ] = (int) $row['tax_class_id'];
            }
        }

        // Unique class IDs referenced by items in this cart.
        $class_ids = array_unique( array_values( $product_class_map ) );

        if ( empty( $class_ids ) ) {
            return [];
        }

        // ── Step 3: Load all applicable rates in one query ───────────────────
        // class_id → rate_rows[]
        $rates_by_class = $this->rate_repo->get_batch_for_classes_in_zones( $class_ids, $zone_ids );

        if ( empty( $rates_by_class ) ) {
            return [];
        }

        // ── Step 4 & 5: Calculate per-class tax amounts ──────────────────────
        // First compute the taxable subtotal per class.
        $subtotal_by_class = [];

        foreach ( $cart['items'] as $item ) {
            $pid       = (int) ( $item['product_id'] ?? 0 );
            $class_id  = $product_class_map[ $pid ] ?? null;

            if ( ! $class_id ) {
                continue;
            }

            // Cart items canonically use 'effective_price' (post-options) or
            // 'unit_price' (before options).  External callers may pass 'price'.
            // Try the most-specific field first.
            $unit_price = (float) ( $item['effective_price']
                ?? $item['unit_price']
                ?? $item['price']
                ?? 0 );
            $line_total = $unit_price * (int) ( $item['quantity'] ?? 1 );
            $subtotal_by_class[ $class_id ] = ( $subtotal_by_class[ $class_id ] ?? 0.0 ) + $line_total;
        }

        $breakdown      = [];
        $lang           = $this->current_language();
        $class_names    = $this->get_class_names_batch( $class_ids, $lang );

        foreach ( $class_ids as $class_id ) {
            $rates    = $rates_by_class[ $class_id ] ?? [];
            $subtotal = $subtotal_by_class[ $class_id ] ?? 0.0;

            if ( empty( $rates ) || $subtotal === 0.0 ) {
                continue;
            }

            $class_tax = $this->apply_rates( $rates, $subtotal );

            if ( $class_tax === 0.0 ) {
                continue;
            }

            // Effective rate (for display) = combined across all rows for this class.
            $effective_rate = $subtotal > 0 ? ( $class_tax / $subtotal ) * 100 : 0;

            $breakdown[] = [
                'class_id'     => $class_id,
                'class_name'   => $class_names[ $class_id ] ?? "Tax Class #{$class_id}",
                'rate_percent' => round( $effective_rate, 4 ),
                'tax_amount'   => round( $class_tax, 4 ),
                'includes_tax' => (bool) ( $rates[0]['includes_tax'] ?? false ),
            ];
        }

        return $breakdown;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // CLASS / ZONE / RATE CRUD (pass-through for module-level convenience)
    // ══════════════════════════════════════════════════════════════════════════

    public function class_repo(): TaxClass {
        static $repo = null;
        if ( ! $repo ) {
            $repo = new TaxClass();
        }
        return $repo;
    }

    public function zone_repo(): TaxZone {
        return $this->zone_repo;
    }

    public function rate_repo(): TaxRate {
        return $this->rate_repo;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // DEFAULT SEEDING
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Seed a default "Standard Rate" tax class on first activation.
     * No rate rows are seeded — the admin must configure the actual rates.
     * Transient-guarded.
     */
    public function maybe_seed_defaults(): void {
        if ( get_transient( 'titancart_tax_seeded' ) ) {
            return;
        }

        $count = (int) $this->db->getField(
            "SELECT COUNT(*) FROM {$this->db->table('tax_classes')}"
        );

        if ( $count > 0 ) {
            set_transient( 'titancart_tax_seeded', 1, 0 );
            return;
        }

        $class_repo = $this->class_repo();

        // ⚠️ `rates_depend_on` is passed EXPLICITLY on purpose. TaxClass::create()
        // carries KI-84's ternary on that field, so omitting the key here would
        // write NULL into a NOT NULL ENUM column. See KI-84.
        $id = $class_repo->create(
            [ 'handle' => 'standard', 'status' => 'active', 'rates_depend_on' => 'shipping' ],
            [ 'en'     => [ 'name' => __( 'Standard Rate', 'titancart-lite' ) ] ]
        );

        // KI-83: the guard must record THE WORK, not merely that we reached the end
        // of the function. Setting it unconditionally makes a failed seed
        // byte-identical to a successful one, and it is never retried.
        if ( $id > 0 ) {
            set_transient( 'titancart_tax_seeded', 1, 0 ); // persist indefinitely — seeded
            return;
        }

        // Failure: retry on a later load, but BOUNDED. This hook runs on
        // `titancart_loaded` (every request), so "only set on success" would
        // re-attempt the INSERT on every page load forever.
        set_transient( 'titancart_tax_seeded', 1, self::SEED_RETRY_DELAY );

        LogService::error(
            'general',
            'seed',
            'Failed to seed the default "Standard Rate" tax class; will retry shortly.',
            [ 'object_type' => 'tax_class', 'retry_in' => self::SEED_RETRY_DELAY ]
        );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // INTERNAL HELPERS
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Apply a set of rate rows (sorted by priority ASC) to a taxable subtotal.
     *
     * Algorithm:
     *   - Group rows by priority.
     *   - Within a priority group, sum the rates together (parallel application).
     *   - Non-compound: apply group rate to the original subtotal.
     *   - Compound: apply group rate to (subtotal + all tax accumulated so far).
     *   - If includes_tax=1: back-calculate embedded tax instead of adding new tax.
     *
     * @param  array[] $rates    Rate rows, sorted priority ASC.
     * @param  float   $subtotal Taxable line-item subtotal (price × qty).
     * @return float   Total tax amount.
     */
    private function apply_rates( array $rates, float $subtotal ): float {
        // Group by priority.
        $by_priority = [];
        foreach ( $rates as $rate ) {
            $by_priority[ (int) $rate['priority'] ][] = $rate;
        }
        ksort( $by_priority );

        $tax_accumulated = 0.0;

        foreach ( $by_priority as $priority_group ) {
            $group_rate     = 0.0;
            $is_compound    = false;
            $includes_tax   = false;

            foreach ( $priority_group as $rate ) {
                // tc_tax_rates.rate is stored in percent (e.g. 6.0000 = 6%).
                // Convert to a fraction here so the math below treats it as a
                // multiplier (subtotal × 0.06), not a 100x scalar (subtotal × 6).
                $group_rate  += (float) $rate['rate'] / 100.0;

                // If ANY row in the group is compound / includes_tax, the whole group is.
                if ( $rate['is_compound'] )  {
                    $is_compound  = true;
                }
                if ( $rate['includes_tax'] ) {
                    $includes_tax = true;
                }
            }

            if ( $group_rate === 0.0 ) {
                continue;
            }

            if ( $includes_tax ) {
                // Price-inclusive: extract the embedded tax.
                // tax = price × rate / (1 + rate)
                $base     = $is_compound ? $subtotal + $tax_accumulated : $subtotal;
                $group_tax = $base * $group_rate / ( 1.0 + $group_rate );
            } elseif ( $is_compound ) {
                // Compound: apply to (subtotal + tax accumulated so far).
                $group_tax = ( $subtotal + $tax_accumulated ) * $group_rate;
            } else {
                // Standard: apply to original subtotal.
                $group_tax = $subtotal * $group_rate;
            }

            $tax_accumulated += $group_tax;
        }

        return $tax_accumulated;
    }

    /**
     * Batch-load translated class names.
     *
     * @param  int[]  $class_ids
     * @param  string $lang
     * @return array  class_id (int) → name string
     */
    private function get_class_names_batch( array $class_ids, string $lang ): array {
        if ( empty( $class_ids ) ) {
            return [];
        }

        $rows = $this->db->getArray(
            "SELECT class_id, name, language_code
             FROM {$this->db->table('tax_class_translations')}
             WHERE class_id IN (?a)
               AND language_code IN (?a)
             ORDER BY class_id ASC,
                      CASE language_code WHEN ?s THEN 0 ELSE 1 END ASC",
            $class_ids,
            [ $lang, 'en' ],
            $lang
        );

        $map = [];
        foreach ( $rows as $row ) {
            $cid = (int) $row['class_id'];
            if ( ! isset( $map[ $cid ] ) || $row['language_code'] === $lang ) {
                $map[ $cid ] = $row['name'];
            }
        }

        return $map;
    }

    /**
     * Detect the current site language.
     *
     * @return string  e.g. 'en'
     */
    private function current_language(): string {
        if ( function_exists( 'pll_current_language' ) ) {
            return pll_current_language() ?: 'en';
        }
        if ( defined( 'ICL_LANGUAGE_CODE' ) && ICL_LANGUAGE_CODE ) {
            return ICL_LANGUAGE_CODE;
        }
        return 'en';
    }
}
