import { injectStyle } from '@descope-ui/common/components-helpers';
import { createStyleMixin, draggableMixin, componentNameValidationMixin } from '../../mixins';
import { createBaseClass } from '../../baseClasses/createBaseClass';
import { compose } from '../../helpers';
import { getComponentName } from '../../helpers/componentHelpers';
import { interpolateString } from './helpers';

export const componentName = getComponentName('policy-validation');

const overrideAttrs = [
  'data-password-policy-value-minlength',
  'data-password-policy-value-passwordstrength',
  'data-password-policy-actual-passwordstrength',
  'data-password-policy-value-disallowedchars',
  'data-password-policy-value-email',
];
const dataAttrs = ['data', 'active-policies', 'overrides', ...overrideAttrs];
const policyAttrs = ['label', 'value', ...dataAttrs];

class RawPolicyValidation extends createBaseClass({ componentName, baseSelector: ':host > div' }) {
  #availablePolicies;

  #activePolicies = [];

  #overrides;

  static get observedAttributes() {
    return policyAttrs;
  }

  constructor() {
    super();

    this.attachShadow({ mode: 'open' }).innerHTML = `
    <div>
      <div class="label"></div>
      <ul></ul>
    </div>
		`;

    injectStyle(
      `
      :host > div {
        width: 100%;
        display: flex;
        flex-direction: column;
        box-sizing: border-box;
      }
      .label {
        max-width: 100%;
        text-wrap: wrap;
        overflow-wrap: break-word;
      }
      .hide-label .label {
        display: none;
      }
      ul {
        display: flex;
        flex-direction: column;
        padding: 0;
        margin: 0;
      }
      ul, li {
        margin: 0;
        padding: 0;
        list-style: none;
      }
      li::before {
        display: inline-block;
        width: 1em;
        text-align: center;
      }
        `,
      this
    );

    this.panel = this.shadowRoot.querySelector(':host > div');
    this.label = this.shadowRoot.querySelector('.label');
    this.list = this.shadowRoot.querySelector('ul');
  }

  attributeChangedCallback(attrName, oldValue, newValue) {
    super.attributeChangedCallback?.(attrName, oldValue, newValue);
    if (oldValue !== newValue) {
      if (attrName === 'label') {
        this.updateLabel(newValue);
      }

      // we're don't know the order in which the attributes are forwarded, so we're trying to render every time
      // once `data` and `active-policies` are populated, the render will be executed.
      // once the `overrides` object is updated, we want to re-render the panel.
      if (dataAttrs.includes(attrName)) {
        if (attrName === 'data') {
          try {
            this.availablePolicies = JSON.parse(newValue);
          } catch {
            // eslint-disable-next-line no-console
            console.error('Failed to set available policies');
          }
        }
        if (attrName === 'active-policies') {
          this.#activePolicies = (newValue || '').split(',');
        }

        if (attrName === 'data-password-policy-value-minlength') {
          const ln = Number(newValue);
          if (!Number.isNaN(ln) && ln > 0) {
            this.#overrides = { ...this.#overrides, minlength: { value: `${ln}` } };
          }
        }

        if (attrName === 'data-password-policy-value-passwordstrength') {
          const passwordStrength = parseInt(newValue);
          if (!Number.isNaN(passwordStrength) && passwordStrength >= 0) {
            this.#overrides = {
              ...this.#overrides,
              passwordstrength: {
                ...this.#overrides?.passwordstrength,
                expected: passwordStrength,
              },
            };
          }
        }

        if (attrName === 'data-password-policy-actual-passwordstrength') {
          const ln = Number(newValue);
          if (!Number.isNaN(ln) && ln >= 0) {
            this.#overrides = {
              ...this.#overrides,
              passwordstrength: { ...this.#overrides?.passwordstrength, actual: ln },
            };
          }
        }

        // disallowedchars: this stores the configured char list in
        // overrides.disallowedchars.value so the message can interpolate
        // `{{value}}`. The regex pattern itself is built upstream (orchestrator
        // or caller) and shipped on the policy entry — it is not derived from
        // this override. When the attribute is cleared, drop the override so
        // the panel doesn't keep stale data.
        if (attrName === 'data-password-policy-value-disallowedchars') {
          if (newValue) {
            this.#overrides = {
              ...this.#overrides,
              disallowedchars: {
                ...this.#overrides?.disallowedchars,
                value: newValue,
              },
            };
          } else if (this.#overrides?.disallowedchars) {
            const { disallowedchars: _drop, ...rest } = this.#overrides;
            this.#overrides = rest;
          }
        }

        // disallowemail: stash the user's email so the STR_NEQ_CI comparator
        // has an `expected` to compare against the live password value. Clear
        // the override when the attribute is removed/empty so we don't keep
        // blocking against a previous user's email.
        if (attrName === 'data-password-policy-value-email') {
          if (newValue) {
            this.#overrides = {
              ...this.#overrides,
              disallowemail: {
                ...this.#overrides?.disallowemail,
                expected: newValue,
              },
            };
          } else if (this.#overrides?.disallowemail) {
            const { disallowemail: _drop, ...rest } = this.#overrides;
            this.#overrides = rest;
          }
        }
      }

      this.renderItems(this.#availablePolicies, this.#activePolicies, this.#overrides);
    }
  }

  get availablePolicies() {
    return this.#availablePolicies || [];
  }

  set availablePolicies(val) {
    this.#availablePolicies = val;
  }

  get value() {
    return this.getAttribute('value') || '';
  }

  getStrengthScore() {
    return this.value.length;
  }

  compareValues(comparison, expected, actual) {
    switch (comparison.toUpperCase()) {
      case 'GTE': // Greater than or equal to
        return actual >= expected;
      case 'GT': // Greater than
        return actual > expected;
      case 'LTE': // Less than or equal to
        return actual <= expected;
      case 'LT': // Less than
        return actual < expected;
      case 'EQ': // Equal to
        return actual === expected;
      case 'NEQ': // Not equal to
        return actual !== expected;
      default:
        throw new Error(`Invalid comparison type: ${comparison}`);
    }
  }

  validate() {
    let policies = this.#availablePolicies;

    if (this.#overrides) {
      policies = this.#availablePolicies.map((policy) => {
        const data = this.#overrides[policy.id];

        if (!data) return policy;

        const res = { ...policy, data };

        return res;
      });
    }

    return this.#activePolicies.reduce((results, id) => {
      const policy = policies.find((p) => p.id === id);

      if (!policy) {
        return results;
      }

      const { pattern, message, data, compare } = policy;
      const normalizedCompare = typeof compare === 'string' ? compare.toUpperCase() : compare;

      if ((!pattern && !compare) || !message) {
        return results;
      }

      const validationResult = {
        message: interpolateString(message, data),
        id,
      };

      if (pattern) {
        const exp = new RegExp(interpolateString(pattern, data));
        validationResult.valid = exp.test(this.value);
      } else if (normalizedCompare === 'STR_NEQ_CI') {
        // Compare the live password against the configured string AND its
        // local-part (before '@'), case-insensitively. Used by the
        // disallowemail policy.
        const expected = (data?.expected ?? '').toLowerCase();
        const actual = (this.value ?? '').toLowerCase();
        if (!expected || !actual) {
          // nothing to compare → mark valid so we don't block the flow
          validationResult.valid = true;
        } else {
          const at = expected.indexOf('@');
          const localPart = at > 0 ? expected.slice(0, at) : expected;
          validationResult.valid = actual !== expected && actual !== localPart;
        }
      } else if (compare) {
        validationResult.valid = this.compareValues(
          normalizedCompare,
          data?.expected ?? -1,
          data?.actual ?? -1
        );
      }

      results.push(validationResult);

      return results;
    }, []);
  }

  get isValid() {
    return !this.validate().some(({ valid }) => valid === false);
  }

  buildValidationItem({ valid, message }) {
    const status = !this.value ? 'none' : valid;
    const li = document.createElement('li');
    li.className = 'item';
    li.dataset.valid = status;
    const span = document.createElement('span');
    span.className = 'message';
    // `textContent` handles any tenant-configured string in `message` safely
    // (e.g. the disallowedchars list) without needing to escape HTML.
    span.textContent = message ?? '';
    li.appendChild(span);
    return li;
  }

  renderItems(availablePolicies, activePolicies) {
    if (!availablePolicies || !activePolicies.length) {
      return;
    }

    this.list.replaceChildren(...this.validate().map(this.buildValidationItem.bind(this)));
  }

  updateLabel(val) {
    if (!val) {
      this.classList.add('hide-label');
      this.label.textContent = '';
    } else {
      this.label.textContent = val;
      this.classList.remove('hide-label');
    }
  }
}

const { host, item, symbolDefault, symbolSuccess, symbolError } = {
  host: { selector: () => ':host > div' },
  item: { selector: () => '.item' },
  symbolDefault: { selector: () => '.item[data-valid="none"]::before' },
  symbolSuccess: { selector: () => '.item[data-valid="true"]::before' },
  symbolError: { selector: () => '.item[data-valid="false"]::before' },
};

export const PolicyValidationClass = compose(
  createStyleMixin({ componentNameOverride: getComponentName('input-wrapper') }),
  createStyleMixin({
    mappings: {
      hostDirection: { selector: () => ':host', property: 'direction' },
      fontSize: {},
      fontFamily: {},
      padding: {},
      borderWidth: { ...host, property: 'border-width' },
      borderStyle: { ...host, property: 'border-style' },
      borderColor: { ...host, property: 'border-color' },
      borderRadius: { ...host, property: 'border-radius' },
      backgroundColor: { ...host, property: 'background-color' },
      textColor: { property: 'color' },
      labelMargin: { ...host, property: 'gap' },
      itemsSpacing: { ...item, property: 'line-height' },
      itemSymbolSuccessColor: { ...symbolSuccess, property: 'color' },
      itemSymbolErrorColor: { ...symbolError, property: 'color' },
      itemSymbolDefault: { ...symbolDefault, property: 'content' },
      itemSymbolSuccess: { ...symbolSuccess, property: 'content' },
      itemSymbolError: { ...symbolError, property: 'content' },
    },
  }),
  draggableMixin,
  componentNameValidationMixin
)(RawPolicyValidation);
