/**
 * States, Cities, and Places for WooCommerce - Enhanced
 * Loads cities and neighborhoods on demand via AJAX with client-side caching.
 * @version 1.0.0
 */
jQuery(function ($) {
  'use strict';

  if (typeof wc_country_select_params === 'undefined' || typeof scpwoo_city_select_params === 'undefined') {
    console.warn('WC States Places: Required parameters not found');
    return false;
  }

  var params = scpwoo_city_select_params;

  // ── Client-side cache with TTL ────────────────────────────────
  // Entries expire after 10 minutes to pick up admin changes
  // without requiring a page reload.
  var CACHE_TTL_MS       = 10 * 60 * 1000; // 10 minutes
  var citiesCache        = {};  // key: "country:state" → { data: array, ts: timestamp }
  var neighborhoodsCache = {};  // key: "country:city"  → { data: array, ts: timestamp }

  function cacheGet(cache, key) {
    var entry = cache[key];
    if (!entry) return undefined;
    if (Date.now() - entry.ts > CACHE_TTL_MS) {
      delete cache[key];
      return undefined;
    }
    return entry.data;
  }

  function cacheSet(cache, key, data) {
    cache[key] = { data: data, ts: Date.now() };
  }

  // Module-level restore-value store keyed by element ID.
  // Survives DOM replacement via toInput() → replaceWith(), which creates a new node
  // and discards all data attached to the old one.
  var scpwooRestoreVals = {};

  // Cache of PHP-pre-rendered neighborhood options, keyed by element ID.
  // Populated in country_to_state_changing (before toInput destroys the SELECT)
  // and consumed by city_changing to skip AJAX entirely.
  var scpwooHoodCache = {};

  // ── Helpers ──────────────────────────────────────────────────

  function escapeAttr(str) {
    if (!str) return '';
    return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  function debounce(func, wait) {
    var timeout;
    return function () {
      var context = this, args = arguments;
      clearTimeout(timeout);
      timeout = setTimeout(function () { func.apply(context, args); }, wait);
    };
  }

  function getEnhancedSelectFormatString() {
    return {
      formatMatches: function (matches) {
        return matches === 1 ? wc_country_select_params.i18n_matches_1 : wc_country_select_params.i18n_matches_n.replace('%qty%', matches);
      },
      formatNoMatches: function () { return wc_country_select_params.i18n_no_matches; },
      formatAjaxError: function () { return wc_country_select_params.i18n_ajax_error; },
      formatInputTooShort: function (input, min) {
        var n = min - input.length;
        return n === 1 ? wc_country_select_params.i18n_input_too_short_1 : wc_country_select_params.i18n_input_too_short_n.replace('%qty%', n);
      },
      formatInputTooLong: function (input, max) {
        var n = input.length - max;
        return n === 1 ? wc_country_select_params.i18n_input_too_long_1 : wc_country_select_params.i18n_input_too_long_n.replace('%qty%', n);
      },
      formatSelectionTooBig: function (limit) {
        return limit === 1 ? wc_country_select_params.i18n_selection_too_long_1 : wc_country_select_params.i18n_selection_too_long_n.replace('%qty%', limit);
      },
      formatLoadMore: function () { return wc_country_select_params.i18n_load_more; },
      formatSearching: function () { return wc_country_select_params.i18n_searching; }
    };
  }

  function hasOptionValue($select, val) {
    var found = false;
    $select.find('option').each(function () {
      if ($(this).val() === val) { found = true; return false; }
    });
    return found;
  }

  // ── Fuzzy matcher for Select2 (powered by Fuse.js) ───────────
  // Tolerates typos: "Akowaa" matches "Akwa", "yaoude" matches "Yaoundé"

  var fuseAvailable = typeof Fuse !== 'undefined';

  /**
   * Build a Select2-compatible matcher that uses Fuse.js for fuzzy matching.
   * Falls back to default Select2 matching if Fuse.js is not loaded.
   */
  /**
   * Strip accents from a string for accent-insensitive comparison.
   */
  function stripAccents(str) {
    if (!str) return '';
    return str.normalize ? str.normalize('NFD').replace(/[\u0300-\u036f]/g, '') : str;
  }

  function fuzzyMatcher(searchParams, data) {
    if (!searchParams.term || !searchParams.term.trim()) {
      return data;
    }
    if (!data.text) {
      return null;
    }

    var term = stripAccents(searchParams.term.toLowerCase().trim());
    var text = stripAccents(data.text.toLowerCase().trim());

    // Exact match or contains
    if (text === term || text.indexOf(term) > -1) {
      return data;
    }

    // Fuzzy only for longer terms (4+ chars) to avoid false positives on short codes
    if (fuseAvailable && term.length >= 4) {
      var fuse = new Fuse([data.text], {
        threshold: 0.3,
        distance: 50,
        ignoreLocation: true,
        minMatchCharLength: 3
      });
      if (fuse.search(searchParams.term).length > 0) {
        return data;
      }
    }

    return null;
  }

  // ── Select2 enhancement ─────────────────────────────────────

  if ($().select2) {
    var initCitySelect2 = function () {
      $('select.city_select:visible').each(function () {
        var $s = $(this);
        if ($s.hasClass('select2-hidden-accessible')) { $s.select2('destroy'); }
        $s.select2($.extend({
          placeholderOption: 'first',
          width: '100%',
          allowClear: false,
          minimumResultsForSearch: 10,
          matcher: fuzzyMatcher
        }, getEnhancedSelectFormatString()))
          .on('select2:open', function () { setTimeout(function () { $('.select2-search__field').focus(); }, 0); });
      });
    };
    initCitySelect2();
    $(document.body).on('city_to_select', initCitySelect2);

    var initNeighborhoodSelect2 = function () {
      $('select.neighborhood_select:visible').each(function () {
        var $s = $(this);
        if ($s.hasClass('select2-hidden-accessible')) { $s.select2('destroy'); }
        $s.select2($.extend({
          placeholderOption: 'first',
          width: '100%',
          allowClear: false,
          minimumResultsForSearch: 10,
          matcher: fuzzyMatcher
        }, getEnhancedSelectFormatString()));
      });
    };
    initNeighborhoodSelect2();
    $(document.body).on('neighborhood_to_select', initNeighborhoodSelect2);
  }

  // ── DOM manipulation helpers ────────────────────────────────

  function toInput($box) {
    if ($box.is('input')) { $box.prop('disabled', false); return; }
    var name = $box.attr('name'), id = $box.attr('id'), ph = $box.attr('placeholder') || '';
    var ariaLabel = $box.attr('aria-label') || '';
    if ($box.hasClass('select2-hidden-accessible')) { $box.select2('destroy'); }
    $box.parent().find('.select2-container').remove();
    $box.replaceWith('<input type="text" class="input-text" name="' + escapeAttr(name) + '" id="' + escapeAttr(id) + '" placeholder="' + escapeAttr(ph) + '" aria-label="' + escapeAttr(ariaLabel) + '" />');
  }

  function disableCity($box) {
    $box.val('').change();
    $box.prop('disabled', true);
  }

  function populateSelect($box, items, cssClass, placeholderText) {
    var id = $box.attr('id');
    // Primary: jQuery .data() (works when the element was not replaced by toInput).
    // Fallback: scpwooRestoreVals keyed by element ID (survives toInput/replaceWith,
    // which creates a new DOM node and loses all .data() on the old one).
    var _savedVal = $box.data('scpwoo-restore-val') || scpwooRestoreVals[id];
    var value = $box.val() || _savedVal || '';
    $box.removeData('scpwoo-restore-val');
    delete scpwooRestoreVals[id];

    if ($box.is('input')) {
      var name = $box.attr('name'), ph = $box.attr('placeholder') || '';
      var ariaLabel = $box.attr('aria-label') || '';
      $box.replaceWith('<select name="' + escapeAttr(name) + '" id="' + escapeAttr(id) + '" class="' + cssClass + '" placeholder="' + escapeAttr(ph) + '" aria-label="' + escapeAttr(ariaLabel) + '"></select>');
      $box = $('#' + id);
    } else {
      $box.prop('disabled', false);
    }

    var opts = '<option value="">' + escapeAttr(placeholderText) + '</option>';
    for (var i = 0; i < items.length; i++) {
      opts += '<option value="' + escapeAttr(items[i]) + '">' + escapeAttr(items[i]) + '</option>';
    }
    $box.html(opts);

    if (hasOptionValue($box, value)) {
      // When restoring a saved value, suppress update_checkout to prevent cascade loop.
      if (_savedVal) {
        $box.data('scpwoo-no-update-checkout', true);
        setTimeout(function () { $box.removeData('scpwoo-no-update-checkout'); }, 300);
      }
      $box.val(value).change();
    } else {
      $box.val('').change();
    }

    return $box;
  }

  // ── AJAX loaders with caching ───────────────────────────────

  // AJAX timeout: 8 seconds — covers slow African mobile networks
  // but doesn't leave the user stuck forever
  var AJAX_TIMEOUT_MS = 8000;

  // In-flight XHR handles — abort stale requests before starting a new one
  var activeCitiesXhr = null;
  var activeHoodsXhr  = null;

  /**
   * Mark the checkout as degraded (text inputs instead of selects).
   * Notifies the server via a session-side flag so the client cannot
   * arbitrarily bypass address validation by posting a hidden field.
   */
  function setDegradedMode(active, reason) {
    if (!active) return;
    $.post(params.ajax_url, {
      action: 'scpwoo_set_degraded_mode',
      nonce: params.nonce,
      reason: reason || 'ajax_failure'
    });
  }

  function loadCities(country, state, callback) {
    var key = country + ':' + (state || '');
    var cached = cacheGet(citiesCache, key);

    if (cached !== undefined) {
      callback(cached);
      return;
    }

    if (activeCitiesXhr) {
      activeCitiesXhr.abort();
      activeCitiesXhr = null;
    }

    activeCitiesXhr = $.ajax({
      url: params.ajax_url,
      type: 'GET',
      dataType: 'json',
      timeout: AJAX_TIMEOUT_MS,
      data: {
        action: 'scpwoo_get_cities',
        nonce: params.nonce,
        country: country,
        state: state
      },
      success: function (response) {
        activeCitiesXhr = null;
        var cities = (response && response.data && response.data.cities) ? response.data.cities : [];
        cacheSet(citiesCache, key, cities);
        callback(cities);
      },
      error: function (xhr, status) {
        activeCitiesXhr = null;
        if (status === 'abort') return;
        // Network failure or timeout — degrade to text input
        if (params.i18n_load_error) {
          $('<p class="woocommerce-error scpwoo-load-error" role="alert">' + params.i18n_load_error + '</p>').insertAfter('select.city_select:first').delay(6000).fadeOut('slow', function () { $(this).remove(); });
        }
        setDegradedMode(true, 'cities_load_failure');
        callback([]);
      }
    });
  }

  function loadNeighborhoods(country, city, callback) {
    var key = country + ':' + city;
    var cached = cacheGet(neighborhoodsCache, key);

    if (cached !== undefined) {
      callback(cached);
      return;
    }

    if (activeHoodsXhr) {
      activeHoodsXhr.abort();
      activeHoodsXhr = null;
    }

    activeHoodsXhr = $.ajax({
      url: params.ajax_url,
      type: 'GET',
      dataType: 'json',
      timeout: AJAX_TIMEOUT_MS,
      data: {
        action: 'scpwoo_get_neighborhoods',
        nonce: params.nonce,
        country: country,
        city: city
      },
      success: function (response) {
        activeHoodsXhr = null;
        var hoods = (response && response.data && response.data.neighborhoods) ? response.data.neighborhoods : [];
        cacheSet(neighborhoodsCache, key, hoods);
        callback(hoods);
      },
      error: function (xhr, status) {
        activeHoodsXhr = null;
        if (status === 'abort') return;
        setDegradedMode(true, 'neighborhoods_load_failure');
        callback([]);
      }
    });
  }

  // ── Cart calculator: inject neighborhood field ──────────────
  // Replaces the former inline <script> from PHP.
  // Only activates if the PHP marker element exists.

  function injectCalcNeighborhood() {
    if ($('#calc_shipping_neighborhood').length) return;
    var $cityField = $('.shipping-calculator-form').find('#calc_shipping_city').closest('p');
    if (!$cityField.length) return;

    var html = '<p class="form-row form-row-wide" id="calc_shipping_neighborhood_field">'
      + '<select name="calc_shipping_neighborhood" id="calc_shipping_neighborhood" class="neighborhood_select" style="width:100%;">'
      + '<option value="">' + escapeAttr(params.i18n_select_neighborhood_text) + '</option>'
      + '</select></p>';
    $cityField.after(html);
  }

  if ($('#scpwoo-calc-neighborhood-marker').length) {
    injectCalcNeighborhood();

    $(document.body).on('updated_wc_div updated_cart_totals updated_shipping_method', function () {
      setTimeout(injectCalcNeighborhood, 50);
    });

    $(document.body).on('scpwoo_reinject_calc_neighborhood', injectCalcNeighborhood);

    $(document).on('click', '.shipping-calculator-button', function () {
      setTimeout(injectCalcNeighborhood, 50);
    });
  }

  // ── City cascade: Country/State → City ──────────────────────

  $('body').on('country_to_state_changing', function (e, country, $container) {
    // Flush caches for the old country — stale data from a different context
    citiesCache = {};
    neighborhoodsCache = {};

    // Save current values BEFORE clearing them so the cascade can restore them.
    var $citybox = $container.find('#billing_city, #shipping_city, #calc_shipping_city');
    var $neighborhoodbox = $container.find('#billing_neighborhood, #shipping_neighborhood, #calc_shipping_neighborhood');
    var _cityVal = $citybox.val();
    var _hoodVal = $neighborhoodbox.val();
    var _hoodId  = $neighborhoodbox.attr('id');
    if (_cityVal) { $citybox.data('scpwoo-restore-val', _cityVal); }
    if (_hoodVal) {
      $neighborhoodbox.data('scpwoo-restore-val', _hoodVal);
      scpwooRestoreVals[_hoodId] = _hoodVal;
    }
    // Cache PHP-pre-rendered neighborhood options BEFORE state_changing calls toInput()
    // and replaces the SELECT. city_changing will use this cache to skip AJAX.
    var _hoodLoadedCity = $neighborhoodbox.attr('data-loaded-city');
    if (_hoodLoadedCity && $neighborhoodbox.is('select')) {
      var _hoodOpts = [];
      $neighborhoodbox.find('option[value!=""]').each(function () { _hoodOpts.push($(this).val()); });
      if (_hoodOpts.length) {
        scpwooHoodCache[_hoodId] = { city: _hoodLoadedCity, options: _hoodOpts };
      }
    }
    $citybox.val('');
    $neighborhoodbox.val('');

    var $statebox = $container.find('#billing_state, #shipping_state, #calc_shipping_state');
    var state = $statebox.val();
    $(document.body).trigger('state_changing', [country, state, $container]);
  });

  $('body').on('change', 'select.state_select, #calc_shipping_state', debounce(function () {
    var $container = $(this).closest('div');
    var country = $container.find('#billing_country, #shipping_country, #calc_shipping_country').val();
    var state = $(this).val();
    $(document.body).trigger('state_changing', [country, state, $container]);
  }, 150));

  $('body').on('state_changing', function (e, country, state, $container) {
    var $citybox = $container.find('#billing_city, #shipping_city, #calc_shipping_city');

    if (!country) {
      toInput($citybox);
      return;
    }

    if (!state) {
      disableCity($citybox);
      return;
    }

    // ── Fast path: skip AJAX when PHP already rendered the correct city list ──
    // PHP sets data-loaded-state on the city <select> when it pre-populates it.
    // After the first AJAX load, JS sets scpwoo-loaded-state.
    // If the current state matches and options are present, re-enable immediately.
    var _loadedFor = $citybox.attr('data-loaded-state') || $citybox.data('scpwoo-loaded-state');
    if (_loadedFor === state && $citybox.is('select') && $citybox.find('option').length > 1) {
      $citybox.prop('disabled', false);
      var _rv = $citybox.data('scpwoo-restore-val') || '';
      $citybox.removeData('scpwoo-restore-val');
      if (_rv && hasOptionValue($citybox, _rv)) {
        $citybox.val(_rv);
        $(document.body).trigger('city_changing', [country, _rv, $container]);
      }
      $(document.body).trigger('city_to_select');
      return;
    }

    // ── Slow path: load cities via AJAX ──
    $citybox.prop('disabled', true);

    loadCities(country, state, function (cities) {
      if (cities.length > 0) {
        populateSelect($citybox, cities, 'city_select', params.i18n_select_city_text);
        $citybox.data('scpwoo-loaded-state', state);
        $(document.body).trigger('city_to_select');
      } else {
        toInput($citybox);
      }
    });
  });

  // Re-trigger city cascade after WC cart AJAX updates
  // (WC replaces the entire shipping calculator HTML, so our selects are lost)
  $(document.body).on('updated_cart_totals', function () {
    var $state = $('#calc_shipping_state');
    if ($state.length && $state.val()) {
      // Re-inject neighborhood field first
      $(document.body).trigger('scpwoo_reinject_calc_neighborhood');
      // Re-cascade: state → city → neighborhood
      setTimeout(function () { $state.trigger('change'); }, 150);
    }
  });

  // ── Neighborhood cascade: City → Neighborhood ───────────────

  $('body').on('change', 'select.city_select', debounce(function () {
    var $container = $(this).closest('form, div');
    var country = $container.find('#billing_country, #shipping_country, #calc_shipping_country').val();
    var city = $(this).val();
    $(document.body).trigger('city_changing', [country, city, $container]);
    // Update checkout shipping when city changes (don't wait for neighborhood).
    // Skip during a restore to prevent cascade loop.
    if (city && !$(this).is('#calc_shipping_city') && !$(this).data('scpwoo-no-update-checkout')) {
      $(document.body).trigger('update_checkout');
    }
  }, 150));

  // Reset neighborhood when state changes
  $('body').on('state_changing', function (e, country, state, $container) {
    var $neighborhoodbox = $container.find('#billing_neighborhood, #shipping_neighborhood, #calc_shipping_neighborhood');
    if (!$neighborhoodbox.length) return;

    if ($neighborhoodbox.is('#calc_shipping_neighborhood')) {
      $neighborhoodbox.html('<option value="">' + params.i18n_select_neighborhood_text + '</option>');
      $neighborhoodbox.val('');
    } else if ($neighborhoodbox.data('scpwoo-skip-toInput')) {
      // city_changing fast path already populated the neighborhood — don't wipe it.
      $neighborhoodbox.removeData('scpwoo-skip-toInput');
    } else {
      toInput($neighborhoodbox);
      $neighborhoodbox.val('');
    }
  });

  $('body').on('city_changing', function (e, country, city, $container) {
    var $neighborhoodbox = $container.find('#billing_neighborhood, #shipping_neighborhood, #calc_shipping_neighborhood');
    if (!$neighborhoodbox.length) return;
    var isCalc = $neighborhoodbox.is('#calc_shipping_neighborhood');

    if (!country || !city) {
      if (isCalc) {
        $neighborhoodbox.html('<option value="">' + params.i18n_select_neighborhood_text + '</option>');
        $('#calc_shipping_neighborhood_field').hide();
      } else {
        toInput($neighborhoodbox);
      }
      return;
    }

    // ── Fast path: use PHP-pre-rendered options (avoid AJAX and toInput flash) ──
    // scpwooHoodCache was populated in country_to_state_changing, before toInput()
    // destroys the original SELECT. Signal state_changing not to call toInput here.
    var _hoodId = $neighborhoodbox.attr('id');
    var _cached = !isCalc && scpwooHoodCache[_hoodId];
    if (_cached && _cached.city === city && _cached.options.length) {
      delete scpwooHoodCache[_hoodId];
      $neighborhoodbox.data('scpwoo-skip-toInput', true);
      populateSelect($neighborhoodbox, _cached.options, 'neighborhood_select', params.i18n_select_neighborhood_text);
      $(document.body).trigger('neighborhood_to_select');
      return;
    }

    // ── Slow path: AJAX ──
    // IMPORTANT: do NOT capture $neighborhoodbox in the closure — state_changing
    // will call toInput() and replace the SELECT with a new INPUT node between
    // now and when the callback fires. Re-find the element inside the callback.
    loadNeighborhoods(country, city, function (hoods) {
      var $currentHood = isCalc
        ? $container.find('#calc_shipping_neighborhood')
        : $container.find('#billing_neighborhood, #shipping_neighborhood');
      if (!$currentHood.length) { return; }

      if (hoods.length > 0) {
        populateSelect($currentHood, hoods, 'neighborhood_select', params.i18n_select_neighborhood_text);
        if (isCalc) {
          $('#calc_shipping_neighborhood_field').show();
        } else {
          $(document.body).trigger('neighborhood_to_select');
        }
      } else {
        if (isCalc) {
          $currentHood.html('<option value="">' + params.i18n_select_neighborhood_text + '</option>');
          $('#calc_shipping_neighborhood_field').hide();
        } else {
          toInput($currentHood);
        }
      }
    });
  });

  // Trigger checkout/cart update when neighborhood changes
  // Use multiple event types + class selector for maximum compatibility with Select2
  var neighborhoodUpdateTimer;
  function triggerShippingUpdate($el) {
    if ($el.data('scpwoo-no-update-checkout')) { return; }
    clearTimeout(neighborhoodUpdateTimer);
    neighborhoodUpdateTimer = setTimeout(function () {
      if ($el.is('#calc_shipping_neighborhood')) {
        $('button[name="calc_shipping"]').trigger('click');
      } else {
        $(document.body).trigger('update_checkout');
      }
    }, 100);
  }

  $('body').on('change', '.neighborhood_select, #billing_neighborhood, #shipping_neighborhood, #calc_shipping_neighborhood', function () {
    if ($(this).val()) triggerShippingUpdate($(this));
  });

  $('body').on('select2:select select2:clear', '.neighborhood_select, #billing_neighborhood, #shipping_neighborhood', function () {
    triggerShippingUpdate($(this));
  });

  // ── Auto-hide country field when store has a single active supported country ──
  //

  if (params.single_country) {
    function hideCountryFields() {
      ['billing', 'shipping'].forEach(function (prefix) {
        var $select = $('#' + prefix + '_country');
        if ($select.length && $select.val() === params.single_country) {
          $select.closest('.form-row')
            .removeClass('scpwoo-country-single')
            .addClass('scpwoo-field-hidden');
        }
      });
    }

    // Immediate — PHP class already made it invisible; this just finalises to display:none.
    // WC scripts (loaded before ours) have already fired their DOM-ready handlers.
    hideCountryFields();

    $(document.body).on('updated_checkout', function () {
      hideCountryFields();
    });
  }

});
