<?php
/**
 * CurrencyService — single source of truth for the storefront's primary currency.
 *
 * Reads from `tc_currencies` (the admin-managed table) instead of the three stale
 * pre-S120 APIs (`SettingsService::get('general','currency_symbol')`,
 * `apply_filters('titancart_currency_symbol')`, the legacy currency-symbol option)
 * which all silently fell through to the `$` default.
 *
 * Usage:
 *   CurrencyService::primary()              Full row: code/name/symbol/symbol_after/decimals/...
 *   CurrencyService::primary_code()         e.g. 'USD'
 *   CurrencyService::primary_symbol()       e.g. '$'
 *   CurrencyService::primary_decimals()     e.g. 2
 *   CurrencyService::format( 49.95 )        '$49.95' or '49,95€' depending on settings
 *
 * The `titancart_currency_symbol` filter is registered in Plugin::register_currency_filter()
 * so existing templates that already call `apply_filters('titancart_currency_symbol', '$')`
 * automatically get the correct symbol with zero template changes.
 *
 * Cache: result of primary() is held in a static property for the request lifetime.
 * CurrenciesController calls flush() on every create/update/delete.
 *
 * @package TitanCart\Services
 */

namespace TitanCart\Services;

defined( 'ABSPATH' ) || exit;

class CurrencyService {

    /**
     * Cached primary currency row.
     *
     * Schema: code, name, symbol, symbol_after, decimals, thousands_sep,
     *         decimal_sep, exchange_rate, is_primary
     *
     * @var array<string,mixed>|null
     */
    private static ?array $primary = null;

    /**
     * Default fallback when no currencies are seeded (shouldn't happen on a real install,
     * but keeps the storefront from showing blank prices if the table is empty).
     *
     * @var array<string,mixed>
     */
    private const DEFAULT_PRIMARY = [
        'code'          => 'USD',
        'name'          => 'US Dollar',
        'symbol'        => '$',
        'symbol_after'  => 0,
        'decimals'      => 2,
        'thousands_sep' => ',',
        'decimal_sep'   => '.',
        'exchange_rate' => 1.0,
        'is_primary'    => 1,
    ];

    /**
     * Get the full primary currency row.
     *
     * Lookup order:
     *   1. WHERE is_primary = 1 AND status = 'active'
     *   2. Fallback: any active currency, ordered by position then id
     *   3. Hard fallback: the DEFAULT_PRIMARY USD row
     *
     * @return array<string,mixed>
     */
    public static function primary(): array {
        if ( self::$primary !== null ) {
            return self::$primary;
        }

        try {
            $db    = titancart_db();
            $table = $db->table( 'currencies' );

            $row = $db->getRow(
                "SELECT code, name, symbol, symbol_after, decimals,
                        thousands_sep, decimal_sep, exchange_rate, is_primary
                 FROM {$table}
                 WHERE is_primary = 1 AND status = 'active'
                 LIMIT 1"
            );

            if ( ! $row ) {
                // No primary set — fall back to any active currency.
                $row = $db->getRow(
                    "SELECT code, name, symbol, symbol_after, decimals,
                            thousands_sep, decimal_sep, exchange_rate, is_primary
                     FROM {$table}
                     WHERE status = 'active'
                     ORDER BY position ASC, id ASC
                     LIMIT 1"
                );
            }
        } catch ( \Throwable $e ) {
            // Table missing (very early boot) or DB error — use the hard fallback.
            $row = null;
        }

        if ( ! is_array( $row ) || empty( $row['code'] ) ) {
            $row = self::DEFAULT_PRIMARY;
        }

        // Normalize types — DB returns strings.
        $row['symbol_after']  = (int) ( $row['symbol_after']  ?? 0 );
        $row['decimals']      = (int) ( $row['decimals']      ?? 2 );
        $row['exchange_rate'] = (float) ( $row['exchange_rate'] ?? 1.0 );
        $row['is_primary']    = (int) ( $row['is_primary']    ?? 1 );

        // Defensive defaults for separator fields (NOT NULL columns, but be safe).
        $row['thousands_sep'] = (string) ( $row['thousands_sep'] ?? ',' );
        $row['decimal_sep']   = (string) ( $row['decimal_sep']   ?? '.' );
        $row['symbol']        = (string) ( $row['symbol']        ?? '$' );
        $row['code']          = (string) ( $row['code']          ?? 'USD' );
        $row['name']          = (string) ( $row['name']          ?? '' );

        self::$primary = $row;
        return self::$primary;
    }

    /**
     * Primary currency ISO 4217 code (e.g. 'USD', 'EUR', 'GBP').
     */
    public static function primary_code(): string {
        return (string) self::primary()['code'];
    }

    /**
     * Primary currency display symbol (e.g. '$', '€', '£').
     */
    public static function primary_symbol(): string {
        return (string) self::primary()['symbol'];
    }

    /**
     * Number of decimal places for the primary currency (typically 2).
     */
    public static function primary_decimals(): int {
        return (int) self::primary()['decimals'];
    }

    /**
     * Whether the symbol renders after the number (true) or before (false).
     */
    public static function symbol_after(): bool {
        return (int) self::primary()['symbol_after'] === 1;
    }

    /**
     * Format an amount as a localized currency string.
     *
     * Honors decimals, decimal_sep, thousands_sep, and symbol_after from tc_currencies.
     *
     * Example: format( 1234.5 ) returns:
     *   USD primary  → "$1,234.50"
     *   EUR primary (symbol_after=1, decimal_sep=',') → "1.234,50€"
     *
     * @param  float|int|string $amount
     * @return string
     */
    public static function format( $amount ): string {
        $c = self::primary();
        $num = number_format(
            (float) $amount,
            (int) $c['decimals'],
            (string) $c['decimal_sep'],
            (string) $c['thousands_sep']
        );
        return $c['symbol_after']
            ? $num . $c['symbol']
            : $c['symbol'] . $num;
    }

    /**
     * Flush the cached primary currency lookup.
     *
     * Called by CurrenciesController after every create/update/delete so the next
     * request reads the new value. Also safe to call from anywhere if the caller
     * suspects the cache is stale.
     */
    public static function flush(): void {
        self::$primary = null;
    }
}
