/* ========================================================================
   Niwa Scheduler - visitor appointment picker

   Public API: window.Niwa.Scheduler.activate(options) -> Promise

   Config is read from `NiwaLF.scheduler` (or `NiwaScheduler` when loaded
   without Lead Form Generator):
     { enabled, services, teamMembers, bufferMinutes, ajaxUrl, nonce }

   UX: segment-by-segment flow mirroring the in-chat product experience
   (assets/js/modules/product-experience.js). One segment is visible at a
   time — date (calendar) → time (slots) → contact (details) → success —
   and segment swaps animate the container block-size the same way the
   product focus panels do. Round back buttons (like
   `.niwa-product-panel__back`) step backwards through the flow. After a
   successful booking the morph collapses, matching the add-to-cart
   behavior of the product card.

   AJAX contract:
   - action=niwa_ai_scheduler_availability
     service_id, team_member_id (optional), month=YYYY-MM, date (optional)
     Response data may contain `days`, `available_dates`, or `slots`.
   - action=niwa_ai_scheduler_book
     service_id, team_member_id (optional), start_at, date, time,
     name, email, phone, session_id
     Response data may contain `appointment`; the entire data object is kept.
   ======================================================================== */

window.Niwa = window.Niwa || {};

window.Niwa.Scheduler = (function () {
  'use strict';

  var _active = null;

  function _config() {
    if (window.NiwaLF && window.NiwaLF.scheduler) return window.NiwaLF.scheduler;
    return window.NiwaScheduler || {};
  }

  function _text(key, fallback) {
    var cfg = _config();
    var bags = [cfg.i18n || {}, (window.NiwaLF && window.NiwaLF.i18n) || {}];
    for (var i = 0; i < bags.length; i++) {
      if (typeof bags[i][key] === 'string' && bags[i][key]) return bags[i][key];
    }
    return fallback;
  }

  function _sessionId() {
    if (window.Niwa && window.Niwa.LeadFormEngine && typeof window.Niwa.LeadFormEngine.getSessionId === 'function') {
      return String(window.Niwa.LeadFormEngine.getSessionId() || '');
    }
    if (window.Niwa && window.Niwa.ChatClient && typeof window.Niwa.ChatClient.getSessionId === 'function') {
      return String(window.Niwa.ChatClient.getSessionId() || '');
    }
    if (window.NiwaAI && window.NiwaAI.session_id) return String(window.NiwaAI.session_id);
    try { return String(localStorage.getItem('niwa_ai_session_id') || ''); } catch (e) { return ''; }
  }

  function _id(value) { return String(value == null ? '' : value); }

  function _allowed(rows, ids) {
    rows = Array.isArray(rows) ? rows : [];
    ids = Array.isArray(ids) ? ids.map(_id) : [];
    if (!ids.length) return rows.filter(function (row) { return row && row.active !== false; });
    return rows.filter(function (row) { return row && ids.indexOf(_id(row.id)) >= 0 && row.active !== false; });
  }

  function _el(tag, cls, text) {
    var node = document.createElement(tag);
    if (cls) node.className = cls;
    if (text !== undefined && text !== null) node.textContent = String(text);
    return node;
  }

  function _dateKey(date) {
    var y = date.getFullYear();
    var m = String(date.getMonth() + 1).padStart(2, '0');
    var d = String(date.getDate()).padStart(2, '0');
    return y + '-' + m + '-' + d;
  }

  function _monthKey(date) { return _dateKey(date).slice(0, 7); }

  function _parseDate(value) {
    var match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})/);
    return match ? new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])) : null;
  }

  function _locale() {
    return _config().locale || document.documentElement.lang || navigator.language || 'en';
  }

  function _formatFullDate(dateKey) {
    var date = _parseDate(dateKey);
    if (!date) return String(dateKey || '');
    return new Intl.DateTimeFormat(_locale(), { weekday: 'long', month: 'long', day: 'numeric' }).format(date);
  }

  function _post(action, fields) {
    var cfg = _config();
    var body = new URLSearchParams();
    body.set('action', action);
    body.set('nonce', cfg.nonce || '');
    Object.keys(fields || {}).forEach(function (key) {
      var value = fields[key];
      if (value !== undefined && value !== null && value !== '') body.set(key, String(value));
    });
    return fetch(cfg.ajaxUrl || (window.NiwaAI && window.NiwaAI.ajax) || '/wp-admin/admin-ajax.php', {
      method: 'POST',
      credentials: 'same-origin',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
      body: body.toString()
    }).then(function (response) {
      return response.json().catch(function () { return null; });
    }).then(function (json) {
      if (json && json.data && json.data.nonce) cfg.nonce = String(json.data.nonce);
      if (!json || json.success !== true) {
        var message = json && json.data && (json.data.message || json.data.error);
        throw new Error(message || _text('schedulerError', 'Something went wrong. Please try again.'));
      }
      return json.data || {};
    });
  }

  function _normalizeSlot(slot, fallbackDate) {
    if (typeof slot === 'string') slot = { start_at: slot };
    slot = slot || {};
    var start = String(slot.start_at || slot.start || slot.datetime || '');
    var date = String(slot.date || (start ? start.slice(0, 10) : fallbackDate || ''));
    var time = String(slot.time || (start.indexOf('T') >= 0 ? start.split('T')[1].slice(0, 5) : (start.match(/\s\d{2}:\d{2}/) || [''])[0].trim()));
    if (!start && date && time) start = date + ' ' + time;
    return {
      start_at: start,
      date: date,
      time: time,
      label: String(slot.label || time || start),
      team_member_id: _id(slot.team_member_id || slot.teamMemberId || ''),
      team_member_name: String(slot.team_member_name || slot.teamMemberName || ''),
      raw: slot
    };
  }

  function _normalizeAvailability(data, requestedDate) {
    var result = { days: {}, explicit: false };
    data = data || {};

    function ensure(date) {
      if (!date) return null;
      if (!result.days[date]) result.days[date] = { available: false, slots: [] };
      return result.days[date];
    }
    function addSlot(slot, date) {
      var normalized = _normalizeSlot(slot, date || requestedDate);
      var row = ensure(normalized.date || requestedDate);
      if (!row) return;
      row.available = true;
      row.slots.push(normalized);
    }

    if (Array.isArray(data.available_dates)) {
      result.explicit = true;
      data.available_dates.forEach(function (date) { var row = ensure(String(date)); if (row) row.available = true; });
    }
    if (Array.isArray(data.days)) {
      result.explicit = true;
      data.days.forEach(function (day) {
        if (typeof day === 'string') { var simple = ensure(day); if (simple) simple.available = true; return; }
        if (!day) return;
        var date = String(day.date || '');
        var row = ensure(date);
        if (!row) return;
        row.available = day.available !== false;
        (Array.isArray(day.slots) ? day.slots : []).forEach(function (slot) { addSlot(slot, date); });
      });
    }
    if (Array.isArray(data.available_days)) {
      result.explicit = true;
      data.available_days.forEach(function (day) {
        if (!day) return;
        var row = ensure(String(day.date || ''));
        if (row) row.available = day.available !== false && Number(day.slot_count || 0) > 0;
      });
    }
    if (data.availability && typeof data.availability === 'object') {
      result.explicit = true;
      Object.keys(data.availability).forEach(function (date) {
        var value = data.availability[date];
        var row = ensure(date);
        if (Array.isArray(value)) {
          value.forEach(function (slot) { addSlot(slot, date); });
          row.available = value.length > 0;
        } else {
          row.available = !!value;
        }
      });
    }
    if (Array.isArray(data.slots)) {
      result.explicit = true;
      data.slots.forEach(function (slot) { addSlot(slot, requestedDate); });
    }
    return result;
  }

  /* After a confirmed booking the morph collapses (same behavior as the
     product card after add-to-cart). Detached from the instance lifecycle
     on purpose: the hosting lead form may destroy the scheduler while the
     success screen is still on screen. When the lead form flow continues
     with another question, the morph stays open. */
  function _scheduleMorphClose() {
    setTimeout(function () {
      var engine = window.Niwa && window.Niwa.LeadFormEngine;
      var formStillRunning = engine
        && typeof engine.isCompleted === 'function' && !engine.isCompleted()
        && typeof engine.getCurrentQuestion === 'function' && !!engine.getCurrentQuestion();
      if (formStillRunning) return;
      var morph = window.Niwa && window.Niwa.MorphController;
      if (morph && typeof morph.isOpen === 'function' && morph.isOpen() && typeof morph.close === 'function') {
        morph.close();
      }
    }, 1600);
  }

  function SchedulerInstance(options) {
    this.options = options || {};
    this.cfg = _config();
    var restrictions = (this.options.question && this.options.question.appointment) || {};
    this.services = _allowed(this.cfg.services, this.options.serviceIds || restrictions.service_ids);
    this.team = _allowed(this.cfg.teamMembers, this.options.teamMemberIds || restrictions.team_member_ids);
    this.serviceId = _id(this.options.serviceId || (this.services[0] && this.services[0].id));
    if (!this.services.some(function (service) { return _id(service.id) === this.serviceId; }, this)) {
      this.serviceId = _id(this.services[0] && this.services[0].id);
    }
    this.teamId = _id(this.options.teamMemberId || '');
    this.month = new Date();
    this.month.setDate(1);
    this.days = {};
    this.explicitAvailability = false;
    this.selectedDate = '';
    this.selectedSlot = null;
    this.step = 'date';
    this.root = null;
    this.live = null;
    this.spinner = null;
    this.content = null;
    this.calendarGrid = null;
    this.ownedHost = false;
    this.shell = null;
    this.docked = false;
    this.overlayHost = null;
    this.overlayClickHandler = null;
    this.overlayKeyHandler = null;
    this.previousFocus = null;
    this.morphBeforeCloseHandler = null;
    this.morphOpenedHandler = null;
    this.restoreOnOpen = false;
    this.resolve = null;
    this.reject = null;
    this.finished = false;
    this.stepAnimation = null;
  }

  SchedulerInstance.prototype.start = function () {
    var self = this;
    return new Promise(function (resolve, reject) {
      self.resolve = resolve;
      self.reject = reject;
      if (self.cfg.enabled === false || !self.services.length) {
        reject(new Error(_text('schedulerUnavailable', 'Online booking is not available right now.')));
        return;
      }
      self._mount();
      self._loadMonth();
    });
  };

  /* Without an explicit host the scheduler docks into the morph shell just
     above the composer pill — the exact placement the product focus uses
     (`_placeFocus` in product-experience.js) — so it REPLACES the input
     instead of scrolling away inside the chat stream. */
  SchedulerInstance.prototype._mount = function () {
    var self = this;
    var host = this.options.host;
    if (this.options.overlay) {
      host = _el('div', 'niwa-widget niwa-booking-overlay');
      var widget = document.querySelector('.niwa-widget[data-niwa-theme]');
      var theme = (widget && widget.getAttribute('data-niwa-theme'))
        || (window.NiwaAI && window.NiwaAI.chat_theme_mode)
        || 'neutral';
      host.setAttribute('data-niwa-theme', theme);
      host.setAttribute('role', 'presentation');
      document.body.appendChild(host);
      document.body.classList.add('niwa-booking-overlay-open');
      this.overlayHost = host;
      this.ownedHost = true;
      this.previousFocus = document.activeElement;
    } else if (!host) {
      var shell = document.querySelector('.niwa-morph, .niwa-center');
      if (shell) {
        var pill = shell.querySelector('.niwa-morph__pill, .niwa-center__pill');
        var dock = _el('div', 'niwa-scheduler-dock');
        if (pill) shell.insertBefore(dock, pill);
        else shell.appendChild(dock);
        shell.classList.add('is-scheduler-open');
        this.shell = shell;
        this.docked = true;
        host = dock;
      } else {
        var body = document.querySelector('.niwa-shortcode__body, .niwa-morph__body, .niwa-center__body');
        if (!body) throw new Error('scheduler_host_missing');
        var wrapper = _el('div', 'niwa-scheduler-message');
        body.appendChild(wrapper);
        host = wrapper;
      }
      this.ownedHost = true;
    }

    var root = _el('section', 'niwa-scheduler');
    root.classList.add('niwa-scheduler--initializing');
    root.setAttribute('aria-label', _text('schedulerTitle', 'Book an appointment'));
    if (!this.options.host || this.options.overlay) {
      root.setAttribute('role', 'dialog');
      if (this.options.overlay) {
        root.classList.add('niwa-scheduler--overlay');
        root.setAttribute('aria-modal', 'true');
        root.setAttribute('tabindex', '-1');
      }
      var head = _el('div', 'niwa-scheduler__header');
      head.appendChild(_el('strong', 'niwa-scheduler__title', this.options.title || _text('schedulerTitle', 'Book an appointment')));
      if (this.options.dismissible !== false) {
        var close = _el('button', 'niwa-scheduler__close', '×');
        close.type = 'button';
        close.setAttribute('aria-label', _text('close', 'Close'));
        close.addEventListener('click', this.destroy.bind(this));
        head.appendChild(close);
      }
      root.appendChild(head);
    }
    this.live = _el('div', 'niwa-scheduler__live');
    this.live.setAttribute('role', 'status');
    this.live.setAttribute('aria-live', 'polite');
    this.live.setAttribute('aria-atomic', 'true');
    root.appendChild(this.live);
    this.spinner = _el('span', 'niwa-scheduler__spinner');
    this.spinner.setAttribute('aria-hidden', 'true');
    root.appendChild(this.spinner);
    this.content = _el('div', 'niwa-scheduler__content');
    root.appendChild(this.content);
    host.appendChild(root);
    this.root = root;

    if (this.overlayHost) {
      this.overlayClickHandler = function (event) {
        if (event.target === self.overlayHost) self.destroy();
      };
      this.overlayKeyHandler = function (event) {
        if (event.key === 'Escape') {
          event.preventDefault();
          self.destroy();
          return;
        }
        if (event.key !== 'Tab' || !self.root) return;
        var focusable = self.root.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), [href], [tabindex]:not([tabindex="-1"])');
        if (!focusable.length) return;
        var first = focusable[0];
        var last = focusable[focusable.length - 1];
        if (event.shiftKey && document.activeElement === first) {
          event.preventDefault();
          last.focus();
        } else if (!event.shiftKey && document.activeElement === last) {
          event.preventDefault();
          first.focus();
        }
      };
      this.overlayHost.addEventListener('click', this.overlayClickHandler);
      document.addEventListener('keydown', this.overlayKeyHandler);
      setTimeout(function () {
        var close = self.root && self.root.querySelector('.niwa-scheduler__close');
        if (close) close.focus();
        else if (self.root) self.root.focus();
      }, 0);
    }

    if (this.docked) {
      root.classList.add('niwa-scheduler--dock');
      root.getBoundingClientRect();
      root.classList.add('is-visible');
      var chatBody = document.querySelector('.niwa-morph__body, .niwa-center__body');
      if (chatBody) chatBody.scrollTop = chatBody.scrollHeight;
      this.morphBeforeCloseHandler = function () { self._pauseForMorph(); };
      this.morphOpenedHandler = function () { self._resumeAfterMorph(); };
      window.addEventListener('niwa:morph:before-close', this.morphBeforeCloseHandler);
      window.addEventListener('niwa:morph:opened', this.morphOpenedHandler);
    }
  };

  SchedulerInstance.prototype._pauseForMorph = function () {
    if (!this.docked || !this.root) return;
    if (this.finished) {
      this.destroy();
      return;
    }
    this.restoreOnOpen = true;
    this.root.hidden = true;
    this.root.classList.remove('is-visible', 'is-closing');
    if (this.shell) this.shell.classList.remove('is-scheduler-open');
  };

  SchedulerInstance.prototype._resumeAfterMorph = function () {
    if (!this.restoreOnOpen || !this.root || this.finished) return;
    this.restoreOnOpen = false;
    if (this.shell) this.shell.classList.add('is-scheduler-open');
    this.root.hidden = false;
    this.root.getBoundingClientRect();
    this.root.classList.add('is-visible');
  };

  SchedulerInstance.prototype._service = function () {
    var id = this.serviceId;
    return this.services.find(function (service) { return _id(service.id) === id; }) || this.services[0];
  };

  SchedulerInstance.prototype._eligibleTeam = function () {
    var service = this._service() || {};
    var memberIds = service.team_member_ids || service.teamMemberIds;
    var ids = Array.isArray(memberIds) ? memberIds.map(_id) : [];
    if (!ids.length) return this.team;
    return this.team.filter(function (member) { return ids.indexOf(_id(member.id)) >= 0; });
  };

  SchedulerInstance.prototype._setBusy = function (busy, message) {
    if (!this.root) return;
    this.root.classList.toggle('niwa-scheduler--busy', !!busy);
    this.root.setAttribute('aria-busy', busy ? 'true' : 'false');
    this.live.classList.toggle('is-error', !busy && !!message);
    this.live.textContent = message || '';
  };

  /* -- Calm segment morph ------------------------------------------------- */

  SchedulerInstance.prototype._clearStepAnimation = function () {
    if (this.stepAnimation) this.stepAnimation.cancel();
    this.stepAnimation = null;
    if (this.content) this.content.style.removeProperty('height');
  };

  SchedulerInstance.prototype._swapStep = function (step, element) {
    var content = this.content;
    if (!content) return;
    var self = this;
    var previousStep = this.step;
    var previous = content.firstElementChild;
    var initial = !previous;
    /* Read the currently painted height before cancelling an in-flight morph,
       so a fast Back/forward click continues from the visible frame. */
    var startSize = content.getBoundingClientRect().height;
    this._clearStepAnimation();
    content.style.height = startSize + 'px';
    if (previous) content.innerHTML = '';
    content.appendChild(element);
    /* Measure the new card at its natural height. `scrollHeight` while the
       old fixed height is still applied cannot report a smaller target. */
    content.style.removeProperty('height');
    var targetSize = content.getBoundingClientRect().height;
    this.step = step;

    /* Service, team and month changes refresh the date screen in place.
       Replaying an entrance animation here made a normal select change feel
       like a new card, so only real step changes morph. */
    if (!initial && previousStep === step) {
      content.style.removeProperty('height');
      return;
    }

    content.style.height = targetSize + 'px';
    var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (typeof content.animate !== 'function' || reducedMotion) {
      content.style.removeProperty('height');
      return;
    }
    var animation = content.animate(
      [
        { height: startSize + 'px', opacity: initial ? 0 : 0.72 },
        { height: targetSize + 'px', opacity: 1 }
      ],
      { duration: 320, easing: 'cubic-bezier(0.2, 0, 0, 1)' }
    );
    this.stepAnimation = animation;
    animation.onfinish = function () {
      if (self.stepAnimation !== animation) return;
      self.stepAnimation = null;
      content.style.removeProperty('height');
    };
  };

  /* Header shared by all segments: round ‹ back button on the left (mirrors
     `.niwa-product-panel__back`), centered eyebrow + title, and a spacer on
     the right so the heading stays optically centered. */
  SchedulerInstance.prototype._stepHeader = function (eyebrow, title, onBack) {
    var header = _el('div', 'niwa-scheduler__step-header');
    if (onBack) {
      var back = _el('button', 'niwa-scheduler__back', '‹');
      back.type = 'button';
      back.setAttribute('aria-label', _text('back', 'Back'));
      back.addEventListener('click', onBack);
      header.appendChild(back);
    } else {
      header.appendChild(_el('span', 'niwa-scheduler__step-spacer'));
    }
    var heading = _el('div', 'niwa-scheduler__step-heading');
    if (eyebrow) heading.appendChild(_el('span', 'niwa-scheduler__eyebrow', eyebrow));
    if (title) heading.appendChild(_el('strong', 'niwa-scheduler__step-title', title));
    header.appendChild(heading);
    header.appendChild(_el('span', 'niwa-scheduler__step-spacer'));
    return header;
  };

  /* -- Segment 1: date ---------------------------------------------------- */

  SchedulerInstance.prototype._showDateStep = function () {
    this._swapStep('date', this._buildDateStep());
  };

  SchedulerInstance.prototype._buildDateStep = function () {
    var self = this;
    var step = _el('section', 'niwa-scheduler__step niwa-scheduler__step--date');
    step.appendChild(this._stepHeader(_text('pickDate', 'Pick a date'), '', null));

    var controls = _el('div', 'niwa-scheduler__controls');
    if (this.services.length) {
      var serviceLabel = _el('label', 'niwa-scheduler__field niwa-scheduler__field--select');
      serviceLabel.appendChild(_el('span', '', _text('service', 'Service')));
      var serviceSelect = _el('select', 'niwa-scheduler__select');
      this.services.forEach(function (service) {
        var option = _el('option', '', service.name || service.label || service.id);
        option.value = _id(service.id);
        option.selected = option.value === self.serviceId;
        serviceSelect.appendChild(option);
      });
      serviceSelect.addEventListener('change', function () {
        self.serviceId = this.value;
        self.teamId = '';
        self.selectedDate = '';
        self.selectedSlot = null;
        self._loadMonth();
      });
      serviceLabel.appendChild(serviceSelect);
      controls.appendChild(serviceLabel);
    }

    var eligibleTeam = this._eligibleTeam();
    if (eligibleTeam.length) {
      var teamLabel = _el('label', 'niwa-scheduler__field niwa-scheduler__field--select');
      teamLabel.appendChild(_el('span', '', _text('teamMember', 'Team member')));
      var teamSelect = _el('select', 'niwa-scheduler__select');
      var any = _el('option', '', _text('anyAvailable', 'Any available'));
      any.value = '';
      teamSelect.appendChild(any);
      eligibleTeam.forEach(function (member) {
        var option = _el('option', '', member.name || member.id);
        option.value = _id(member.id);
        option.selected = option.value === self.teamId;
        teamSelect.appendChild(option);
      });
      teamSelect.addEventListener('change', function () {
        self.teamId = this.value;
        self.selectedDate = '';
        self.selectedSlot = null;
        self._loadMonth();
      });
      teamLabel.appendChild(teamSelect);
      controls.appendChild(teamLabel);
    }
    if (controls.childElementCount) {
      controls.classList.toggle('niwa-scheduler__controls--single', controls.childElementCount === 1);
      step.appendChild(controls);
    }

    var nav = _el('div', 'niwa-scheduler__month-nav');
    var prev = _el('button', 'niwa-scheduler__nav', '‹');
    prev.type = 'button';
    prev.setAttribute('aria-label', _text('previousMonth', 'Previous month'));
    var todayMonth = new Date(); todayMonth.setDate(1); todayMonth.setHours(0, 0, 0, 0);
    prev.disabled = this.month <= todayMonth;
    prev.addEventListener('click', function () { self.month.setMonth(self.month.getMonth() - 1); self._loadMonth(); });
    nav.appendChild(prev);
    nav.appendChild(_el('strong', 'niwa-scheduler__month', new Intl.DateTimeFormat(_locale(), { month: 'long', year: 'numeric' }).format(this.month)));
    var next = _el('button', 'niwa-scheduler__nav', '›');
    next.type = 'button';
    next.setAttribute('aria-label', _text('nextMonth', 'Next month'));
    next.addEventListener('click', function () { self.month.setMonth(self.month.getMonth() + 1); self._loadMonth(); });
    nav.appendChild(next);
    step.appendChild(nav);

    var grid = _el('div', 'niwa-scheduler__calendar');
    grid.setAttribute('role', 'grid');
    var monday = new Date(2024, 0, 1);
    for (var wd = 0; wd < 7; wd++) {
      var weekday = _el('span', 'niwa-scheduler__weekday', new Intl.DateTimeFormat(_locale(), { weekday: 'short' }).format(new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + wd)));
      weekday.setAttribute('role', 'columnheader');
      grid.appendChild(weekday);
    }
    var firstOffset = (this.month.getDay() + 6) % 7;
    for (var blank = 0; blank < firstOffset; blank++) grid.appendChild(_el('span', 'niwa-scheduler__day niwa-scheduler__day--blank'));
    var count = new Date(this.month.getFullYear(), this.month.getMonth() + 1, 0).getDate();
    var startToday = new Date(); startToday.setHours(0, 0, 0, 0);
    for (var day = 1; day <= count; day++) {
      (function () {
        var date = new Date(self.month.getFullYear(), self.month.getMonth(), day);
        var key = _dateKey(date);
        var info = self.days[key];
        var isPast = date < startToday;
        var available = !isPast && !!(info && info.available);
        var btn = _el('button', 'niwa-scheduler__day', String(day));
        btn.type = 'button';
        btn.setAttribute('role', 'gridcell');
        btn.setAttribute('data-date', key);
        btn.setAttribute('aria-label', _formatFullDate(key));
        btn.disabled = !available;
        btn.classList.toggle('niwa-scheduler__day--available', available);
        btn.classList.toggle('niwa-scheduler__day--muted', !available);
        btn.classList.toggle('niwa-scheduler__day--selected', self.selectedDate === key);
        btn.setAttribute('aria-pressed', self.selectedDate === key ? 'true' : 'false');
        if (available) btn.addEventListener('click', function () { self._selectDate(key); });
        grid.appendChild(btn);
      })();
    }
    step.appendChild(grid);
    this.calendarGrid = grid;
    if (!Object.keys(this.days).some(function (key) { return self.days[key] && self.days[key].available; })) {
      step.appendChild(_el('p', 'niwa-scheduler__empty', _text('noDates', 'No available dates in this month.')));
    }
    return step;
  };

  SchedulerInstance.prototype._loadMonth = function () {
    var self = this;
    this._setBusy(true, _text('loadingAvailability', 'Loading available dates...'));
    _post('niwa_ai_scheduler_availability', {
      service_id: this.serviceId,
      team_member_id: this.teamId,
      month: _monthKey(this.month)
    }).then(function (data) {
      var normalized = _normalizeAvailability(data, '');
      self.days = normalized.days;
      self.explicitAvailability = normalized.explicit;
      self._showDateStep();
      self.root.classList.remove('niwa-scheduler--initializing');
      self._setBusy(false, '');
    }).catch(function (error) {
      self.days = {};
      self.explicitAvailability = true;
      self._showDateStep();
      self.root.classList.remove('niwa-scheduler--initializing');
      self._setBusy(false, error.message);
    });
  };

  SchedulerInstance.prototype._markSelectedDay = function (key) {
    if (!this.calendarGrid) return;
    this.calendarGrid.querySelectorAll('.niwa-scheduler__day[data-date]').forEach(function (btn) {
      var selected = btn.getAttribute('data-date') === key;
      btn.classList.toggle('niwa-scheduler__day--selected', selected);
      btn.setAttribute('aria-pressed', selected ? 'true' : 'false');
    });
  };

  SchedulerInstance.prototype._selectDate = function (date) {
    var self = this;
    this.selectedDate = date;
    this.selectedSlot = null;
    this._markSelectedDay(date);
    var current = this.days[date];
    if (current && current.slots.length) {
      this._showTimeStep(current.slots);
      return;
    }
    this._setBusy(true, _text('loadingTimes', 'Loading available times...'));
    _post('niwa_ai_scheduler_availability', {
      service_id: this.serviceId,
      team_member_id: this.teamId,
      date: date
    }).then(function (data) {
      var normalized = _normalizeAvailability(data, date);
      var info = normalized.days[date] || { available: false, slots: [] };
      self.days[date] = info;
      self._setBusy(false, '');
      self._showTimeStep(info.slots);
    }).catch(function (error) {
      self._setBusy(false, error.message);
      self._showTimeStep([]);
    });
  };

  /* -- Segment 2: time ---------------------------------------------------- */

  SchedulerInstance.prototype._showTimeStep = function (slots) {
    var self = this;
    slots = slots.filter(function (slot) { return Number(String(slot.time || '').split(':')[1]) % 30 === 0; });
    var step = _el('section', 'niwa-scheduler__step niwa-scheduler__step--time');
    step.appendChild(this._stepHeader(
      _text('pickTime', 'Pick a time'),
      _formatFullDate(this.selectedDate),
      function () { self._showDateStep(); }
    ));
    var list = _el('div', 'niwa-scheduler__slots');
    if (!slots.length) {
      list.appendChild(_el('p', 'niwa-scheduler__empty', _text('noTimes', 'No available times for this date.')));
    }
    slots.forEach(function (slot) {
      var btn = _el('button', 'niwa-scheduler__slot', slot.label);
      btn.type = 'button';
      btn.setAttribute('aria-pressed', 'false');
      btn.addEventListener('click', function () {
        btn.setAttribute('aria-pressed', 'true');
        self.selectedSlot = slot;
        self._showContactStep();
      });
      list.appendChild(btn);
    });
    step.appendChild(list);
    this._swapStep('time', step);
  };

  /* -- Segment 3: contact details ----------------------------------------- */

  SchedulerInstance.prototype._showContactStep = function () {
    var self = this;
    var service = this._service() || {};
    var step = _el('section', 'niwa-scheduler__step niwa-scheduler__step--contact');
    step.appendChild(this._stepHeader(
      '',
      _text('yourDetails', 'Your details'),
      function () { self._showTimeStep((self.days[self.selectedDate] || {}).slots || []); }
    ));

    var appointmentContext = _el('p', 'niwa-scheduler__appointment-context');
    appointmentContext.textContent = [
      service.name || service.label || '',
      _formatFullDate(this.selectedDate),
      this.selectedSlot ? (this.selectedSlot.time || this.selectedSlot.label) : ''
    ].filter(Boolean).join(' · ');
    appointmentContext.title = appointmentContext.textContent;
    step.appendChild(appointmentContext);

    var form = _el('form', 'niwa-scheduler__contact');
    form.noValidate = true;
    var fields = [
      { key: 'name', type: 'text', autocomplete: 'name', label: _text('name', 'Name') },
      { key: 'phone', type: 'tel', autocomplete: 'tel', label: _text('phone', 'Phone') },
      { key: 'email', type: 'email', autocomplete: 'email', label: _text('email', 'Email') }
    ];
    var configuredFields = Array.isArray(this.cfg.contactFields)
      ? this.cfg.contactFields.map(String)
      : fields.map(function (spec) { return spec.key; });
    fields = fields.filter(function (spec) { return configuredFields.indexOf(spec.key) >= 0; });
    fields.forEach(function (spec) {
      var label = _el('label', 'niwa-scheduler__field');
      label.appendChild(_el('span', '', spec.label));
      var input = _el('input', 'niwa-scheduler__input');
      input.type = spec.type;
      input.name = spec.key;
      input.autocomplete = spec.autocomplete;
      input.required = true;
      label.appendChild(input);
      form.appendChild(label);
    });
    var error = _el('p', 'niwa-scheduler__form-error');
    error.setAttribute('role', 'alert');
    form.appendChild(error);
    var submit = _el('button', 'niwa-scheduler__primary', _text('confirmBooking', 'Confirm booking'));
    submit.type = 'submit';
    form.appendChild(submit);
    form.addEventListener('submit', function (event) {
      event.preventDefault();
      if (!form.reportValidity()) return;
      error.textContent = '';
      submit.disabled = true;
      var contact = { name: '', phone: '', email: '' };
      fields.forEach(function (spec) {
        contact[spec.key] = form.elements[spec.key].value.trim();
      });
      self._book(contact).catch(function (err) { error.textContent = err.message; submit.disabled = false; });
    });
    step.appendChild(form);
    this._swapStep('contact', step);
    setTimeout(function () { if (fields[0] && form.elements[fields[0].key]) form.elements[fields[0].key].focus(); }, 60);
  };

  /* -- Segment 4: success -------------------------------------------------- */

  SchedulerInstance.prototype._showSuccessStep = function () {
    var step = _el('section', 'niwa-scheduler__step niwa-scheduler__step--success');
    var badge = _el('div', 'niwa-scheduler__success-badge');
    badge.setAttribute('aria-hidden', 'true');
    badge.innerHTML = '<svg viewBox="0 0 24 24"><path d="M5 12.5l4.2 4.2L19 7"></path></svg>';
    step.appendChild(badge);
    step.appendChild(_el('p', 'niwa-scheduler__success-text', _text('bookingConfirmed', 'Your appointment is confirmed.')));
    this._swapStep('success', step);
  };

  SchedulerInstance.prototype._book = function (contact) {
    var self = this;
    var slot = this.selectedSlot;
    // Keep "Any available" unpinned so the server can atomically try every
    // eligible member. Pin only when the visitor explicitly chose a member.
    var teamId = this.teamId || '';
    this._setBusy(true, _text('booking', 'Booking your appointment...'));
    return _post('niwa_ai_scheduler_book', {
      service_id: this.serviceId,
      team_member_id: teamId,
      start_at: slot.start_at,
      date: slot.date || this.selectedDate,
      time: slot.time,
      name: contact.name || '',
      email: contact.email || '',
      phone: contact.phone || '',
      session_id: _sessionId()
    }).then(function (data) {
      self._setBusy(false, '');
      var appointment = data.appointment || data.booking || data;
      var service = self._service() || {};
      var answer = [
        _text('appointmentAnswerPrefix', 'Appointment'),
        service.name || service.label || self.serviceId,
        (slot.date || self.selectedDate) + ' ' + (slot.time || slot.label),
        contact.name,
        contact.phone,
        contact.email
      ].filter(Boolean).join(' | ');
      var result = {
        appointment: appointment,
        answer: answer,
        service: service,
        teamMemberId: teamId,
        slot: slot,
        contact: contact
      };
      self.finished = true;
      self._showSuccessStep();
      try { document.dispatchEvent(new CustomEvent('niwa:scheduler:booked', { detail: result })); } catch (e) { /* noop */ }
      if (typeof self.options.onBooked === 'function') {
        try { self.options.onBooked(result); } catch (callbackError) { /* booking already succeeded */ }
      }
      if (self.resolve) self.resolve(result);
      if (self.options.overlay) {
        setTimeout(function () { if (_active === self) self.destroy(); }, 1600);
      } else {
        _scheduleMorphClose();
      }
      return result;
    }).catch(function (error) {
      self._setBusy(false, '');
      throw error;
    });
  };

  SchedulerInstance.prototype.destroy = function () {
    this._clearStepAnimation();
    this.restoreOnOpen = false;
    if (this.overlayHost && this.overlayClickHandler) this.overlayHost.removeEventListener('click', this.overlayClickHandler);
    if (this.overlayKeyHandler) document.removeEventListener('keydown', this.overlayKeyHandler);
    this.overlayClickHandler = null;
    this.overlayKeyHandler = null;
    document.body.classList.remove('niwa-booking-overlay-open');
    if (this.morphBeforeCloseHandler) window.removeEventListener('niwa:morph:before-close', this.morphBeforeCloseHandler);
    if (this.morphOpenedHandler) window.removeEventListener('niwa:morph:opened', this.morphOpenedHandler);
    this.morphBeforeCloseHandler = null;
    this.morphOpenedHandler = null;
    if (this.shell) {
      this.shell.classList.remove('is-scheduler-open');
      this.shell = null;
    }
    if (this.root) {
      var root = this.root;
      var host = root.parentNode;
      var ownedHost = this.ownedHost;
      if (this.docked && root.classList.contains('is-visible')) {
        root.classList.remove('is-visible');
        root.classList.add('is-closing');
        setTimeout(function () {
          root.remove();
          if (ownedHost && host) host.remove();
        }, 260);
      } else {
        root.remove();
        if (ownedHost && host) host.remove();
      }
    }
    this.root = null;
    this.content = null;
    this.calendarGrid = null;
    this.overlayHost = null;
    if (this.previousFocus && typeof this.previousFocus.focus === 'function') this.previousFocus.focus();
    this.previousFocus = null;
    if (!this.finished && this.resolve) {
      this.finished = true;
      this.resolve({ cancelled: true });
    }
    if (_active === this) _active = null;
  };

  function activate(options) {
    if (_active) _active.destroy();
    _active = new SchedulerInstance(options || {});
    var promise;
    try { promise = _active.start(); }
    catch (error) { promise = Promise.reject(error); }
    promise.catch(function (error) {
      try { document.dispatchEvent(new CustomEvent('niwa:scheduler:error', { detail: { error: error } })); } catch (e) { /* noop */ }
    });
    return promise;
  }

  function destroy() { if (_active) _active.destroy(); }

  function openOverlay(options) {
    options = options || {};
    options.overlay = true;
    return activate(options);
  }

  function _bookingTrigger(target) {
    if (!target || typeof target.closest !== 'function') return null;
    var trigger = target.closest('[data-niwa-booking], a[href*="#niwa-booking"]');
    if (!trigger) return null;
    if (trigger.hasAttribute('data-niwa-booking')) return trigger;
    try { return new URL(trigger.href, window.location.href).hash === '#niwa-booking' ? trigger : null; }
    catch (error) { return null; }
  }

  function _openFromTrigger(trigger) {
    return openOverlay({
      serviceId: trigger ? trigger.getAttribute('data-niwa-booking-service') || '' : '',
      teamMemberId: trigger ? trigger.getAttribute('data-niwa-booking-team-member') || '' : '',
      title: trigger ? trigger.getAttribute('data-niwa-booking-title') || '' : ''
    });
  }

  if (document.addEventListener) {
    document.addEventListener('click', function (event) {
      var trigger = _bookingTrigger(event.target);
      if (!trigger) return;
      event.preventDefault();
      _openFromTrigger(trigger).catch(function () {});
    });
    if (window.addEventListener && window.location) {
      window.addEventListener('hashchange', function () {
        if (window.location.hash === '#niwa-booking') _openFromTrigger(null).catch(function () {});
      });
      if (window.location.hash === '#niwa-booking') {
        setTimeout(function () { _openFromTrigger(null).catch(function () {}); }, 0);
      }
    }
  }

  return { activate: activate, openOverlay: openOverlay, destroy: destroy };
})();
