<?php

namespace auctionforge\inc\helpers;

/**
 * Regional spelling dialect for the word "catalog" / "catalogue".
 *
 *   US dialect -> "Catalog"   (the plugin's authored strings, left unchanged)
 *   UK dialect -> "Catalogue"
 *
 * The front-end swap is performed with a `gettext` filter scoped to the
 * `auctionforge` text domain, so no template files need editing. Because the
 * filter only rewrites translated, user-facing strings it leaves every code
 * identifier, CSS class (e.g. `af-catalog-sidebar`, `catalog-section`),
 * JS variable and option key (e.g. `catalog_layout`, `bids_in_catalog`)
 * completely untouched.
 *
 * The dialect is chosen in: AuctionForge -> Settings -> Appearance.
 */
class Dialect
{
    const OPTION = 'spelling_dialect';
    const US = 'us';
    const UK = 'uk';
    const AUTO = 'auto';

    /**
     * Default mode for sites that have not chosen one: Auto (match the auction
     * house region). Resolves to FALLBACK_DIALECT until the first sync.
     */
    const DEFAULT_DIALECT = self::AUTO;

    /** Concrete fallback when Auto can't resolve yet, or for any unknown value. */
    const FALLBACK_DIALECT = self::UK;

    /** Auto mode: dialect resolved from the auction-house region (set at sync). */
    const OPTION_AUTO_RESOLVED = 'spelling_dialect_auto';      // 'us' | 'uk'
    const OPTION_AUTO_REGION   = 'spelling_dialect_auto_region'; // detected label, for admin display

    /**
     * Two-letter USPS state/territory codes, as a regex alternation, for
     * recognising "City, ST 12345" address tails. Matched case-sensitively:
     * lowercase words like "in" must not count as Indiana.
     */
    const US_STATE_CODES = 'AL|AK|AZ|AR|CA|CO|CT|DE|DC|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|PR|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY';

    /** @var string|null Cached, normalised dialect for the current request. */
    private static $dialect = null;

    /**
     * Active dialect for this site: self::US or self::UK.
     * @return string
     */
    public static function get()
    {
        if (self::$dialect === null) {
            $value = SettingsHelper::getOption(self::OPTION, self::DEFAULT_DIALECT);
            if ($value === self::US) {
                self::$dialect = self::US;
            } elseif ($value === self::AUTO) {
                // Resolved from the auction-house region during sync; fall back to
                // a concrete dialect until the first sync populates it.
                $auto = get_option(self::OPTION_AUTO_RESOLVED, '');
                self::$dialect = ($auto === self::US) ? self::US : (($auto === self::UK) ? self::UK : self::FALLBACK_DIALECT);
            } else {
                self::$dialect = self::UK;
            }
        }
        return self::$dialect;
    }

    /**
     * @return bool True when the active dialect is UK ("Catalogue").
     */
    public static function isUk()
    {
        return self::get() === self::UK;
    }

    /**
     * Map an auction-house country (currency as a fallback) to a dialect. Only
     * the United States uses American "Catalog"; every other country uses
     * British "Catalogue".
     *
     * US addresses usually name no country at all and end in "City, ST 12345",
     * so the last comma-segment ("NY 10001") is a state+ZIP, not a country.
     * Recognise that signature — otherwise such houses fall through to the
     * "unknown named country" branch and wrongly resolve to Catalogue.
     *
     * @param string $country  last address segment, e.g. "United Kingdom", "NY 10001"
     * @param string $currency e.g. "£", "$" — secondary signal only
     * @param string $address  full house address, e.g. "New York, NY 10001"
     * @return string self::US | self::UK
     */
    public static function countryToDialect($country, $currency = '', $address = '')
    {
        $c = strtolower(trim((string) $country));
        if ($c !== '') {
            foreach (array('united states', 'usa', 'u.s.a', 'u.s.', 'america') as $hint) {
                if (strpos($c, $hint) !== false) {
                    return self::US;
                }
            }
            if ($c === 'us') {
                return self::US;
            }
        }
        $probe = trim((string) $address) !== '' ? (string) $address : (string) $country;
        // "… ST 12345" / "… ST 12345-6789" tail: US state code + full 5-digit
        // ZIP. Requiring all 5 digits keeps Australian "WA 6000" / "SA 5000"
        // (4-digit postcodes) and Canadian "ON M5V 3L9" from matching.
        if (preg_match('/\b(' . self::US_STATE_CODES . ')[.\s]+\d{5}(?:-\d{4})?[\s.]*$/', $probe)) {
            return self::US;
        }
        // Secondary: a bare trailing 5-digit ZIP plus a dollar currency
        // ("Brooklyn 11201" + "$") — no country name, no state code.
        if (preg_match('/\b\d{5}(?:-\d{4})?[\s.]*$/', $probe) && strpos((string) $currency, '$') !== false) {
            return self::US;
        }
        if ($c !== '') {
            return self::UK; // any other named country -> Catalogue
        }
        // No country in the address — best-effort from the currency symbol.
        return (strpos((string) $currency, '$') !== false) ? self::US : self::UK;
    }

    /**
     * Capture the auction-house region from a getAllAuctions response and store
     * the resolved "Auto" dialect. Called during the auction sync, so page loads
     * never trigger an API call. Harmless to run regardless of the chosen mode.
     *
     * @param array|object $auctions decoded `response` list of auctions
     */
    public static function captureRegionFromAuctions($auctions)
    {
        $country = '';
        $currency = '';
        $address = '';
        foreach ((array) $auctions as $a) {
            $addr = is_object($a) ? (isset($a->address) ? $a->address : '') : (isset($a['address']) ? $a['address'] : '');
            $cur  = is_object($a) ? (isset($a->currency) ? $a->currency : '') : (isset($a['currency']) ? $a['currency'] : '');
            if ($currency === '' && $cur !== '') {
                $currency = $cur;
            }
            if ($addr !== '') {
                $parts = array_map('trim', explode(',', $addr));
                $country = (string) end($parts);
                if ($country !== '') {
                    $address = (string) $addr;
                    break;
                }
            }
        }
        update_option(self::OPTION_AUTO_RESOLVED, self::countryToDialect($country, $currency, $address), 'no');
        update_option(self::OPTION_AUTO_REGION, $country !== '' ? $country : trim('currency ' . $currency), 'no');
        self::$dialect = null; // invalidate per-request cache
    }

    /**
     * Return the catalog word in the active dialect, matching the casing and
     * pluralisation of $sample. Use this for any string that is NOT routed
     * through __() (the gettext filter handles everything that is).
     *
     * @param string $sample e.g. 'Catalog', 'catalog', 'CATALOG', 'Catalogs'
     * @return string
     */
    public static function word($sample = 'Catalog')
    {
        return self::isUk() ? self::toUk($sample) : $sample;
    }

    /**
     * `gettext` filter callback. Swaps US -> UK spelling in auctionforge
     * front-end strings.
     *
     * @param string $translation
     * @param string $text   (unused, kept for the filter signature)
     * @param string $domain
     * @return string
     */
    public static function filterGettext($translation, $text, $domain)
    {
        if ($domain !== 'auctionforge') {
            return $translation;
        }
        // Only the public-facing site, never the wp-admin settings UI. This
        // keeps admin field labels as authored and, importantly, keeps the
        // dialect dropdown's own "Catalog" / "Catalogue" option labels literal
        // so the choice stays self-explanatory. wp_doing_ajax() guards the
        // admin-ajax case (is_admin() is true there even for front-end AJAX).
        if (is_admin() && !wp_doing_ajax()) {
            return $translation;
        }
        return self::toUk($translation);
    }

    /**
     * `gettext_with_context` filter callback (4-arg signature).
     *
     * @param string $translation
     * @param string $text
     * @param string $context
     * @param string $domain
     * @return string
     */
    public static function filterGettextWithContext($translation, $text, $context, $domain)
    {
        return self::filterGettext($translation, $text, $domain);
    }

    /**
     * Replace whole-word "catalog"/"catalogs" with British "catalogue"/
     * "catalogues", preserving case (lower / Title / UPPER) and pluralisation.
     * Idempotent: input that already reads "catalogue" is returned unchanged,
     * and words such as "cataloged" / "subcatalog" are left alone.
     *
     * @param string $text
     * @return string
     */
    public static function toUk($text)
    {
        if ($text === '' || stripos($text, 'catalog') === false) {
            return $text;
        }
        return preg_replace_callback('/\bcatalog(ue)?(s)?\b/i', function ($m) {
            $matched = $m[0];
            $plural  = (substr(strtolower($matched), -1) === 's');
            $british = $plural ? 'catalogues' : 'catalogue';

            if ($matched === strtoupper($matched)) {
                return strtoupper($british);   // CATALOG(S) -> CATALOGUE(S)
            }
            if ($matched[0] === strtoupper($matched[0])) {
                return ucfirst($british);      // Catalog(S) -> Catalogue(s)
            }
            return $british;                   // catalog(s) -> catalogue(s)
        }, $text);
    }

    /**
     * Register the front-end gettext swap. No-op (zero added overhead) when the
     * active dialect is US, so US sites pay nothing.
     */
    public static function register()
    {
        if (!self::isUk()) {
            return;
        }
        add_filter('gettext', array(__CLASS__, 'filterGettext'), 10, 3);
        add_filter('gettext_with_context', array(__CLASS__, 'filterGettextWithContext'), 10, 4);
    }
}
