import { AsYouType, parsePhoneNumberFromString } from 'libphonenumber-js/min';
import { createBaseInputClass } from '../../../../baseClasses/createBaseInputClass';
import { forwardAttrs, getComponentName } from '../../../../helpers/componentHelpers';
import CountryCodes from '../../CountryCodes';
import { comboBoxItem } from '../helpers';

export const componentName = getComponentName('phone-field-internal');

const commonAttrs = ['disabled', 'size', 'bordered', 'readonly'];
const countryAttrs = ['country-input-placeholder', 'default-code', 'restrict-countries'];
const phoneAttrs = [
  'phone-input-placeholder',
  'maxlength',
  'autocomplete',
  'name',
  'phone-input-type',
];
const labelTypeAttrs = ['label-type', 'country-input-label', 'label'];
const mapAttrs = {
  'country-input-label': 'label',
  'country-input-placeholder': 'placeholder',
  'phone-input-placeholder': 'placeholder',
  'phone-input-type': 'type',
};

const inputRelatedAttrs = [].concat(commonAttrs, countryAttrs, phoneAttrs, labelTypeAttrs);

const BaseInputClass = createBaseInputClass({ componentName, baseSelector: 'div' });

class PhoneFieldInternal extends BaseInputClass {
  static get observedAttributes() {
    return [].concat(BaseInputClass.observedAttributes || [], inputRelatedAttrs);
  }

  #ayt;

  constructor() {
    super();

    this.innerHTML = `
    <div class="wrapper">
      <descope-combo-box
        item-label-path="data-name"
        item-value-path="data-id"
      >
      ${CountryCodes.map((item) => comboBoxItem(item)).join('')}
      </descope-combo-box>
      <div class="separator"></div>
      <descope-text-field type="tel"></descope-text-field>
    </div>
    `;

    this.comboBox = this.querySelector('descope-combo-box');
    this.textField = this.querySelector('descope-text-field');

    this.inputs = [this.comboBox, this.textField];

    forwardAttrs(this, this.comboBox, { includeAttrs: ['label-type'] });
    forwardAttrs(this, this.textField, {
      includeAttrs: ['label-type', 'required', 'phone-input-type'],
      mapAttrs: { 'phone-input-type': 'type' },
    });

    // override combo box setter to display dialCode value in input
    this.comboBox.customValueTransformFn = (val) => {
      const [, dialCode] = val?.split?.(' ') || [];
      return dialCode;
    };
  }

  // exposed from main component
  get countryCodeInputData() {
    return this.comboBox.items;
  }

  // exposed from main component
  get countryCodeValue() {
    return this.comboBox.shadowRoot.querySelector('input').value;
  }

  // exposed from main component
  get phoneNumberInputEle() {
    return this.textField.shadowRoot.querySelector('input');
  }

  get allowAlphanumericInput() {
    return this.getAttribute('allow-alphanumeric-input') === 'true';
  }

  get defaultCode() {
    return this.getAttribute('default-code');
  }

  get selectionStart() {
    return this.textField.selectionStart;
  }

  get minLength() {
    return parseInt(this.getAttribute('minlength'), 10) || 0;
  }

  get selectedCountryCode() {
    return this.comboBox?.selectedItem?.getAttribute('data-country-code');
  }

  get restrictCountries() {
    const attr = this.getAttribute('restrict-countries');
    return attr?.split(',').filter(Boolean) || [];
  }

  get isFormatValue() {
    return this.getAttribute('format-value') === 'true';
  }

  // `strict validation` enforces value parsing with libphonenumber-js
  get isStrictValidation() {
    return this.getAttribute('strict-validation') === 'true';
  }

  get value() {
    if (!this.comboBox.value || !this.textField.value) {
      return '';
    }

    if (this.allowAlphanumericInput) {
      return `${this.comboBox.value}-${this.textField.value}`;
    }

    return `${this.comboBox.value}-${this.textField.value.replace(/\D+/g, '')}`;
  }

  #clearValue() {
    this.comboBox.selectedItem = undefined;
    this.textField.value = '';
  }

  set value(val) {
    this.#onValueChange(val);
  }

  // Handles programmatic value changes only (e.g. `component.value = '...'`).
  // User edits of the inner inputs are handled by #onTextFieldChange /
  // #onComboBoxChange — Vaadin's value write-back is disabled in PhoneFieldClass
  // so it no longer routes inner edits through this setter.
  #onValueChange(val) {
    // reject empty or digit-free values
    if (!val || !/\d/.test(val)) {
      this.#clearValue();
      return;
    }

    // ensure plus prefix
    if (!val.startsWith('+')) {
      val = `+${val}`;
    }

    let dialCode = '';
    let nationalNumber = '';

    if (val.includes('-')) {
      // dash explicitly marks where the dial code ends — no parsing needed
      const dashIdx = val.indexOf('-');
      dialCode = val.slice(0, dashIdx);
      nationalNumber = val.slice(dashIdx + 1).replace(/\D+/g, '');
    } else {
      // parse only to get dial code length,
      const parsed = parsePhoneNumberFromString(val);
      if (parsed?.countryCallingCode) {
        dialCode = `+${parsed.countryCallingCode}`;
        // slice the raw value to avoid parser transformations
        nationalNumber = val.slice(dialCode.length).replace(/\D+/g, '');
      }
    }

    // dial code must contain digits (e.g. '+1', not just '+')
    if (!/\d/.test(dialCode)) {
      this.#clearValue();
      return;
    }

    // Collect all country codes sharing this dial code from CountryCodes (e.g. US, CA, DO, ... for +1)
    const matchingCountryCodes = CountryCodes.filter((c) => c.dialCode === dialCode).map(
      (c) => c.code
    );

    // Check if the currently selected country's data-country-code is in that set
    const currentSelectedCountryCode = this.selectedCountryCode;
    if (!matchingCountryCodes.includes(currentSelectedCountryCode)) {
      // If not, find the first combo box item (restricted list) whose data-country-code is in the set
      const matchingItem = this.comboBox.items?.find((c) =>
        matchingCountryCodes.includes(c.getAttribute('data-country-code'))
      );
      if (matchingItem) {
        this.comboBox.selectedItem = matchingItem;
      }
    }

    // set formatted or raw national number on the text field
    if (this.isFormatValue) {
      this.textField.value = this.#formatNationalNumber(nationalNumber);
    } else {
      this.textField.value = nationalNumber;
    }
  }

  init() {
    this.addEventListener('focus', (e) => {
      // we want to ignore focus events we are dispatching
      if (e.isTrusted) this.inputs[1].focus();
    });

    super.init?.();

    this.#initInputs();
  }

  getValidity() {
    const countryCode = this.comboBox.value;
    const nationalNumer = this.textField.value;

    const isEmpty = !countryCode || !nationalNumer;
    const isValidLength = nationalNumer && nationalNumer.length >= this.minLength;

    if (this.isRequired && isEmpty) {
      return { valueMissing: true };
    }

    if (this.value) {
      if (!isValidLength) {
        return { tooShort: true };
      }

      if (this.isStrictValidation && !this.#isValidParsedValue()) {
        return { patternMismatch: true };
      }
    }

    return {};
  }

  setSelectionRange(...args) {
    this.textField.setSelectionRange(...args);
  }

  attributeChangedCallback(attrName, oldValue, newValue) {
    super.attributeChangedCallback(attrName, oldValue, newValue);

    if (oldValue !== newValue) {
      if (attrName === 'default-code' && newValue && !this.selectedCountryCode) {
        this.#handleDefaultCountryCode(newValue);
      } else if (inputRelatedAttrs.includes(attrName)) {
        const attr = mapAttrs[attrName] || attrName;

        if (commonAttrs.includes(attrName)) {
          if (!newValue) {
            this.inputs.forEach((input) => input.removeAttribute(attr));
          } else {
            this.inputs.forEach((input) => input.setAttribute(attr, newValue));
          }
        } else if (countryAttrs.includes(attrName)) {
          this.comboBox.setAttribute(attr, newValue);
        } else if (phoneAttrs.includes(attrName)) {
          this.textField.setAttribute(attr, newValue);
        }
      }

      if (labelTypeAttrs.includes(attrName)) {
        this.#handleLabelTypeAttrs(attrName, newValue);
      }

      if (attrName === 'restrict-countries') {
        this.#updateComboBoxItems(this.restrictCountries);
      }
    }
  }

  #initInputs() {
    // Handle each inner input on its own: the phone field updates the national
    // number, the combo box updates the country/dial code. Neither rewrites the
    // other, so editing one input never clears the other.
    this.textField.addEventListener('input', this.#onTextFieldChange.bind(this));
    this.comboBox.addEventListener('change', this.#onComboBoxChange.bind(this));

    this.handleFocusEventsDispatching(this.inputs);
    this.handleInputEventDispatching();
  }

  // user edited the phone number: sanitize and (optionally) format it, leaving
  // the country code untouched
  #onTextFieldChange() {
    const sanitized = this.allowAlphanumericInput
      ? this.textField.value
      : this.textField.value.replace(/\D+/g, '');

    this.textField.value = this.isFormatValue ? this.#formatNationalNumber(sanitized) : sanitized;
  }

  // user changed the country: re-format the existing national number for the
  // new country, leaving the phone number digits untouched
  #onComboBoxChange() {
    if (!this.isFormatValue) {
      return;
    }

    const nationalNumber = this.textField.value.replace(/\D+/g, '');
    this.textField.value = this.#formatNationalNumber(nationalNumber);
  }

  #formatNationalNumber(nationalNumber = '') {
    // re-initialize AsYouType if country code is outdated
    if (!this.#ayt || this.#ayt.country !== this.selectedCountryCode) {
      this.#ayt = new AsYouType(this.selectedCountryCode);
    }

    // reset previous AsYouType input
    this.#ayt.reset();

    const formattedVal = this.#ayt.input(nationalNumber);
    return formattedVal || nationalNumber;
  }

  #isValidParsedValue() {
    const parsed = parsePhoneNumberFromString(this.value);

    return (
      !!parsed && // Parsed successfully (not undefined)
      !!parsed.isValid?.() && // Parsed object is valid
      !!parsed.country && // Parsed object with a country code
      !!this.#isAllowedCountry(parsed.country) // Parsed with allowed country code
    );
  }

  #isAllowedCountry(countryCode) {
    if (!this.restrictCountries.length) {
      return true;
    }

    return this.restrictCountries.includes(countryCode);
  }

  // return country item by country code `data-country-code` (e.g. `US`)
  #getCountryItemByCodeId(countryCode) {
    return this.comboBox.items?.find((c) => c.getAttribute('data-country-code') === countryCode);
  }

  #updateComboBoxItems(restrictCountries) {
    const items = restrictCountries.length
      ? CountryCodes.filter((c) => restrictCountries.includes(c.code))
      : CountryCodes;

    this.querySelector('descope-combo-box').innerHTML = items
      .map((item) => comboBoxItem(item))
      .join('');
  }

  #handleDefaultCountryCode(countryCode) {
    if (!this.comboBox.value) {
      const countryCodeItem = this.#getCountryItemByCodeId(countryCode);
      // When replacing the input component (inserting internal component into text-field) -
      // Vaadin resets the input's value. We use setTimeout in order to make sure this happens
      // after the reset.
      if (countryCodeItem) {
        setTimeout(() => {
          this.comboBox.selectedItem = countryCodeItem;
        });
      }
    }
  }

  #handleLabelTypeAttrs(attrName, newValue) {
    // set or remove label attributes from inner text/combo components on `label-type` change
    const attr = mapAttrs[attrName] || attrName;

    if (attrName === 'label-type') {
      this.#handleLabelTypeChange(newValue);
    }
    // on inner components label attr change - set label attributes for text/combo components
    // only if label-type is `floating`
    else if (this.getAttribute('label-type') === 'floating') {
      if (attrName === 'country-input-label') {
        this.comboBox.setAttribute(attr, newValue);
      } else if (attrName === 'label') {
        this.textField.setAttribute(attr, newValue);
      }
    }
  }

  #handleLabelTypeChange(newValue) {
    if (newValue === 'floating') {
      // on change to `floating` label - set inner components `label` and `placeholder`
      this.comboBox.setAttribute('label', this.getAttribute('country-input-label') || '');
      this.comboBox.setAttribute(
        'placeholder',
        this.getAttribute('country-input-placeholder') || ''
      );
      this.textField.setAttribute('label', this.getAttribute('label') || '');
      this.textField.setAttribute(
        'placeholder',
        this.getAttribute('phone-input-placeholder') || ''
      );
    } else {
      // for other cases - reset inner components label-type and labels
      this.inputs.forEach((input) => input.removeAttribute('label'));
    }
  }
}

export default PhoneFieldInternal;
