/* Find a Tender Forms - UI Test - Complete React Application */

const { useState, useEffect, useCallback, useRef, useMemo } = React;

// ============================================================
// GLOBAL APP CONFIG (shared between components)
// ============================================================
window.APP_CONFIG = window.APP_CONFIG || {
  jwt: '',
  apiUrl: '',
  env: 'tpp',       // 'tpp' or 'prod'
  sender: {          // SENDER block config for XML envelope (hardcoded — matches Blazor reference)
    esenderLogin: 'TED123',
    docExtRef: '',      // Auto-set from JWT noticeId on load
    organisation: 'SourceDogg (GEC)',
    country: 'UK',
    email: 'customerservice@sourcedogg.com'
  }
};

// ============================================================
// SETUP PANEL — JWT, Environment, API URL
// ============================================================
function SetupPanel({ onContinue }) {
  // Auto-extract JWT from URL hash/query on first render
  const urlJwt = useRef(window.ApiService?.extractJwtFromUrl?.() || null);
  const [jwt, setJwt] = useState(urlJwt.current || window.APP_CONFIG.jwt || '');
  const [apiUrl, setApiUrl] = useState(window.APP_CONFIG.apiUrl || '');
  const [env, setEnv] = useState(window.APP_CONFIG.env || 'tpp');
  const [jwtInfo, setJwtInfo] = useState(null);

  // Auto-decode JWT when pasted
  useEffect(() => {
    if (jwt && jwt.split('.').length === 3) {
      const claims = window.ApiService?.decodeJwt(jwt);
      if (claims) {
        setJwtInfo(claims);
        // Auto-fill API URL from iss claim if not manually set
        if (claims.iss && !apiUrl) {
          setApiUrl(claims.iss);
        }
        // Auto-set SENDER docExtRef from JWT noticeId
        if (claims.noticeId) {
          if (!window.APP_CONFIG.sender) window.APP_CONFIG.sender = {};
          window.APP_CONFIG.sender.docExtRef = `${new Date().getFullYear()}-${String(claims.noticeId).padStart(6, '0')}`;
        }
      } else {
        setJwtInfo(null);
      }
    } else {
      setJwtInfo(null);
    }
  }, [jwt]);

  const handleContinue = (formId) => {
    window.APP_CONFIG.jwt = jwt;
    window.APP_CONFIG.apiUrl = apiUrl;
    window.APP_CONFIG.env = env;
    // SENDER config is hardcoded in APP_CONFIG (matches Blazor reference)
    onContinue(formId);
  };

  return (
    <div className="form-selector">
      <h1>Find a Tender Forms</h1>
      <p className="subtitle">UI Test - Configure & Select Form</p>

      {/* Setup Section */}
      <div className="setup-section">
        <h3 style={{marginBottom:'1rem',fontSize:'1rem',fontWeight:600}}>Configuration</h3>

        {/* Environment Toggle */}
        <div className="setup-field">
          <label>Gov API Environment</label>
          <div className="env-toggle">
            <button
              className={`env-btn ${env === 'tpp' ? 'active' : ''}`}
              onClick={() => setEnv('tpp')}
            >
              TPP (Test)
            </button>
            <button
              className={`env-btn ${env === 'prod' ? 'active' : ''}`}
              onClick={() => setEnv('prod')}
            >
              Production
            </button>
          </div>
          <span className="setup-hint">
            {env === 'tpp' ? 'www-tpp.find-tender.service.gov.uk' : 'www.find-tender.service.gov.uk'}
          </span>
        </div>

        {/* JWT Token */}
        <div className="setup-field">
          <label>JWT Token <span className="optional-badge">Optional</span></label>
          <textarea
            className="setup-input"
            rows={3}
            value={jwt}
            onChange={e => setJwt(e.target.value.trim())}
            placeholder="Paste your JWT token here for Save/Publish features..."
            style={{fontFamily:'monospace',fontSize:'0.75rem'}}
          />
          {jwtInfo && (
            <div className="jwt-info">
              <span className="jwt-valid">Valid JWT</span>
              {jwtInfo.iss && <span>Issuer: {jwtInfo.iss.substring(0, 50)}...</span>}
              {jwtInfo.exp && (
                <span>
                  Expires: {new Date(jwtInfo.exp * 1000).toLocaleString()}
                  {Date.now() > jwtInfo.exp * 1000 ? ' (EXPIRED)' : ''}
                </span>
              )}
            </div>
          )}
        </div>

        {/* API URL */}
        <div className="setup-field">
          <label>SourceDogg API URL <span className="optional-badge">Optional</span></label>
          <input
            type="text"
            className="setup-input"
            value={apiUrl}
            onChange={e => setApiUrl(e.target.value.trim())}
            placeholder="Auto-detected from JWT 'iss' claim, or paste manually..."
          />
          <span className="setup-hint">Required for Save/Publish. Auto-filled from JWT if available.</span>
        </div>

        {/* SENDER config is hardcoded (matches Blazor reference project) — no UI needed */}
      </div>

      {/* Form Cards */}
      <div className="form-cards">
        <div className="form-card" onClick={() => handleContinue('UK6')}>
          <h3>UK6</h3>
          <p>Contract Award Notice</p>
          <p style={{fontSize:'0.8rem',marginTop:'0.5rem',opacity:0.6}}>13 pages, ~131 questions</p>
        </div>
        <div className="form-card" onClick={() => handleContinue('UK12')}>
          <h3>UK12</h3>
          <p>Procurement Termination Notice</p>
          <p style={{fontSize:'0.8rem',marginTop:'0.5rem',opacity:0.6}}>5 pages, 11 questions</p>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// VALIDATION MESSAGE
// ============================================================
function ValidationMessage({ errors, warnings }) {
  const hasErrors = errors && errors.length > 0;
  const hasWarnings = warnings && warnings.length > 0;
  if (!hasErrors && !hasWarnings) return null;
  return (
    <div>
      {hasErrors && errors.map((err, i) => (
        <div key={`e${i}`} className="validation-message">{err}</div>
      ))}
      {hasWarnings && warnings.map((warn, i) => (
        <div key={`w${i}`} className="validation-message" style={{color: '#856404', backgroundColor: '#fff3cd', borderColor: '#ffc107'}}>{warn}</div>
      ))}
    </div>
  );
}

// ============================================================
// QUESTION TYPE COMPONENTS
// ============================================================

function TextQuestion({ question, value, onChange, errors, pageId }) {
  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      <input
        type="text"
        className={`form-control ${errors?.length ? 'invalid' : ''}`}
        value={value || ''}
        onChange={e => onChange(question.Id, e.target.value, pageId)}
        placeholder=""
      />
      <ValidationMessage errors={errors} />
    </div>
  );
}

function MultiLineTextQuestion({ question, value, onChange, errors, pageId }) {
  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      <textarea
        className={`form-control ${errors?.length ? 'invalid' : ''}`}
        rows={4}
        value={value || ''}
        onChange={e => onChange(question.Id, e.target.value, pageId)}
      />
      <ValidationMessage errors={errors} />
    </div>
  );
}

function RadioQuestion({ question, options, selectedOptions, onOptionChange, errors, pageId, formEngine }) {
  const selected = selectedOptions || new Set();
  const evaluateOptVis = (opt) => {
    if (!opt.VisibilityRules) return true;
    const optValue = opt.XmlValue || opt.Choice;
    const resolvedRules = JSON.parse(JSON.stringify(opt.VisibilityRules).replace(/\{this\}/g, optValue));
    return window.VisibilityEngine.evaluate(resolvedRules, formEngine, pageId);
  };
  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      {options.map((opt, i) => {
        const optValue = opt.XmlValue || opt.Choice;
        const isVisible = evaluateOptVis(opt);
        if (!isVisible) return null;
        return (
          <div key={i}>
            <div className="form-check">
              <input
                className="form-check-input"
                type="radio"
                name={`${pageId}_${question.Id}`}
                id={`${pageId}_${question.Id}_${i}`}
                checked={selected.has(optValue)}
                onChange={() => onOptionChange(question.Id, optValue, true, pageId, false, 'Radio')}
              />
              <label className="form-check-label" htmlFor={`${pageId}_${question.Id}_${i}`}>
                {opt.Choice}
              </label>
            </div>
            {opt.Hint?.Text && <span className="option-hint">{opt.Hint.Text}</span>}
          </div>
        );
      })}
      <ValidationMessage errors={errors} />
    </div>
  );
}

function CheckboxQuestion({ question, options, selectedOptions, onOptionChange, errors, pageId, formEngine }) {
  const selected = selectedOptions || new Set();

  // Resolve {this} template in option visibility rules before evaluating
  const evaluateOptVisibility = (opt) => {
    if (!opt.VisibilityRules) return true;
    const optValue = opt.XmlValue || opt.Choice;
    // Deep-clone rules and replace {this} with the actual option value
    const resolvedRules = JSON.parse(JSON.stringify(opt.VisibilityRules).replace(/\{this\}/g, optValue));
    return window.VisibilityEngine.evaluate(resolvedRules, formEngine, pageId);
  };

  // Get visible options for bulk actions
  const visibleOpts = options.filter(opt => evaluateOptVisibility(opt));

  // Auto-select when there is exactly 1 visible option and nothing is selected.
  // Only for multi-option questions (e.g., lot pickers) where options are dynamically
  // filtered down to 1. Do NOT auto-select for single-option toggle checkboxes
  // (like R100 "Add additional identifier" or R71 "Additional procurement category")
  // because those are optional toggles the user should explicitly check.
  const hasMultipleDefinedOptions = question.OptionsLookup || (question.Options && question.Options.length > 1);
  useEffect(() => {
    if (hasMultipleDefinedOptions && visibleOpts.length === 1 && selected.size === 0) {
      const opt = visibleOpts[0];
      const val = opt.XmlValue || opt.Choice;
      onOptionChange(question.Id, val, true, pageId, opt.IsExclusive, 'Checkbox');
    }
  }, [visibleOpts.length, selected.size, hasMultipleDefinedOptions]);

  const selectAll = () => {
    visibleOpts.forEach(opt => {
      const val = opt.XmlValue || opt.Choice;
      if (!selected.has(val)) {
        onOptionChange(question.Id, val, true, pageId, opt.IsExclusive, 'Checkbox');
      }
    });
  };

  const deselectAll = () => {
    visibleOpts.forEach(opt => {
      const val = opt.XmlValue || opt.Choice;
      if (selected.has(val)) {
        onOptionChange(question.Id, val, false, pageId, opt.IsExclusive, 'Checkbox');
      }
    });
  };

  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      {visibleOpts.length >= 3 && (
        <div className="bulk-actions" style={{marginBottom:'0.5rem'}}>
          <button type="button" className="btn-link" onClick={selectAll}>Select all</button>
          <span style={{margin:'0 0.5rem', color:'#999'}}>|</span>
          <button type="button" className="btn-link" onClick={deselectAll}>Deselect all</button>
        </div>
      )}
      {options.map((opt, i) => {
        const optValue = opt.XmlValue || opt.Choice;
        const isVisible = evaluateOptVisibility(opt);
        if (!isVisible) return null;
        return (
          <div key={i}>
            <div className="form-check">
              <input
                className="form-check-input darker-border-checkbox"
                type="checkbox"
                id={`${pageId}_${question.Id}_${i}`}
                checked={selected.has(optValue)}
                onChange={e => onOptionChange(question.Id, optValue, e.target.checked, pageId, opt.IsExclusive, 'Checkbox')}
              />
              <label className="form-check-label" htmlFor={`${pageId}_${question.Id}_${i}`}>
                {opt.Choice}
              </label>
            </div>
            {opt.Hint?.Text && <span className="option-hint">{opt.Hint.Text}</span>}
          </div>
        );
      })}
      <ValidationMessage errors={errors} />
    </div>
  );
}

function DropdownQuestion({ question, options, selectedOptions, onOptionChange, errors, pageId }) {
  const selected = selectedOptions || new Set();
  const currentValue = selected.size > 0 ? [...selected][0] : '';
  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      <select
        className={`form-select ${errors?.length ? 'invalid' : ''}`}
        value={currentValue}
        onChange={e => onOptionChange(question.Id, e.target.value, true, pageId, false, 'Dropdown')}
      >
        <option value="">Select...</option>
        {options.map((opt, i) => {
          const optValue = opt.XmlValue || opt.Choice;
          return <option key={i} value={optValue}>{opt.Choice}</option>;
        })}
      </select>
      <ValidationMessage errors={errors} />
    </div>
  );
}

function DateQuestion({ question, value, onChange, errors, pageId }) {
  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      <input
        type="date"
        className={`form-control ${errors?.length ? 'invalid' : ''}`}
        value={value || ''}
        onChange={e => onChange(question.Id, e.target.value, pageId)}
      />
      <ValidationMessage errors={errors} />
    </div>
  );
}

function LabelQuestion({ question, formEngine, pageId }) {
  // Resolve {OptionLabel} template - used on Tenders page (R120) to show lot name
  let label = question.Label;
  if (label.includes('{OptionLabel}') && formEngine && pageId) {
    const page = (formEngine.visiblePages || []).find(p => p.instanceId === pageId);
    if (page && page.optionId) {
      // Try to get full lot name from OCID data
      const ocidData = formEngine.ocidData?.['R4'];
      const lot = ocidData?.lots?.find(l => String(l.id) === String(page.optionId));
      const lotLabel = lot ? `${lot.id}. ${lot.title}` : page.optionLabel || page.optionId;
      label = label.replace('{OptionLabel}', lotLabel);
    }
  }
  return (
    <div className="question-group">
      <div className="label-question">
        <div className="question-label">{label}</div>
        {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      </div>
    </div>
  );
}

function OcidQuestion({ question, value, onChange, errors, warnings, pageId, formEngine }) {
  const [status, setStatus] = useState(null);
  const [loading, setLoading] = useState(false);
  const [ocidDetails, setOcidDetails] = useState(null);

  const handleValidate = async () => {
    if (!value) return;
    setLoading(true);
    setStatus(null);
    setOcidDetails(null);

    try {
      const env = window.APP_CONFIG?.env || 'tpp';

      // 1. Try real API first
      let result = null;
      if (window.OcidService) {
        result = await window.OcidService.fetchOcid(value, env);
      }

      // 2. If API returned error or data, handle accordingly
      if (result && !result.error) {
        // Success from real API
        formEngine.setOcidData(question.Id, value, result);
        setStatus({
          type: 'success',
          message: `Linked to ${result.linksTo?.join(', ') || 'unknown'} - ${result.procurementMethodDetails || 'N/A'}`
        });
        setOcidDetails(result);
      } else {
        // Real API failed — try mock fallback
        const mockResult = formEngine.setOcidData(question.Id, value);
        if (mockResult) {
          setStatus({
            type: 'success',
            message: `[MOCK] Linked to ${mockResult.formName} - ${mockResult.procurementMethodDetails}`,
          });
          setOcidDetails(mockResult);
        } else {
          const apiMsg = result?.message || 'OCID not found';
          setStatus({ type: 'error', message: apiMsg });
        }
      }
    } catch (err) {
      // Fallback to mock on any error
      const mockResult = formEngine.setOcidData(question.Id, value);
      if (mockResult) {
        setStatus({
          type: 'success',
          message: `[MOCK FALLBACK] Linked to ${mockResult.formName} - ${mockResult.procurementMethodDetails}`
        });
        setOcidDetails(mockResult);
      } else {
        setStatus({ type: 'error', message: `Error: ${err.message}` });
      }
    }

    setLoading(false);
  };

  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      <div className="ocid-input-group">
        <input
          type="text"
          className={`form-control ${errors?.length ? 'invalid' : ''}`}
          value={value || ''}
          onChange={e => { onChange(question.Id, e.target.value, pageId); setStatus(null); setOcidDetails(null); }}
          placeholder="ocds-h6vhtk-..."
        />
        <button
          className="sd-style"
          onClick={handleValidate}
          disabled={!value || loading}
          style={{whiteSpace:'nowrap'}}
        >
          {loading ? 'Fetching...' : 'Validate OCID'}
        </button>
      </div>
      {status && (
        <div className={`ocid-status ${status.type}`}>
          {status.type === 'success' ? '\u2713 ' : '\u2717 '}{status.message}
        </div>
      )}
      {ocidDetails && !ocidDetails.error && (
        <div className="ocid-details">
          <div className="ocid-detail-row"><strong>Above Threshold:</strong> {ocidDetails.aboveThreshold ? 'Yes' : 'No'}</div>
          <div className="ocid-detail-row"><strong>Procurement:</strong> {ocidDetails.procurementMethodDetails}</div>
          <div className="ocid-detail-row"><strong>Links To:</strong> {ocidDetails.linksTo?.join(', ') || 'None'}</div>
          {ocidDetails.linksTo?.includes('UK4') && <div className="ocid-detail-row"><strong>Lots:</strong> {ocidDetails.lots?.length || 0}</div>}
          <div className="ocid-detail-row"><strong>Contracts:</strong> {ocidDetails.pendingContractsCount || 0}</div>
          {ocidDetails.unawardedLotsCount > 0 && <div className="ocid-detail-row"><strong>Unawarded Lots:</strong> {ocidDetails.unawardedLotsCount}</div>}
          {ocidDetails.isAQUDM && <div className="ocid-detail-row"><strong>QUDM:</strong> Yes</div>}
          {ocidDetails.hasOpenTypeFrameworkAgreement && <div className="ocid-detail-row"><strong>Framework:</strong> Yes</div>}
          {ocidDetails.hasDynamicMarket && <div className="ocid-detail-row"><strong>Dynamic Market:</strong> Yes</div>}
        </div>
      )}
      <ValidationMessage errors={errors} warnings={warnings} />
    </div>
  );
}

function SirsiSearchQuestion({ question, value, onChange, errors, pageId, formEngine }) {
  const [searchQuery, setSearchQuery] = useState('');
  const [results, setResults] = useState([]);
  const [showDropdown, setShowDropdown] = useState(false);
  const [selectedOrg, setSelectedOrg] = useState(null);
  const [loading, setLoading] = useState(false);
  const dropdownRef = useRef(null);
  const debounceRef = useRef(null);

  // Close dropdown when clicking outside
  useEffect(() => {
    const handleClickOutside = (e) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
        setShowDropdown(false);
      }
    };
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const handleSearch = (query) => {
    setSearchQuery(query);
    onChange(question.Id, query, pageId);
    setSelectedOrg(null);

    // Clear previous debounce
    if (debounceRef.current) clearTimeout(debounceRef.current);

    if (query.length < 2) {
      setResults([]);
      setShowDropdown(false);
      return;
    }

    debounceRef.current = setTimeout(async () => {
      setLoading(true);
      const matches = await window.SirsiService.search(query);
      setResults(matches);
      setShowDropdown(matches.length > 0);
      setLoading(false);
    }, 300);
  };

  const handleSelect = (org) => {
    setSearchQuery(org.name);
    setSelectedOrg(org);
    setShowDropdown(false);
    onChange(question.Id, org.name, pageId);

    // Auto-populate related fields
    if (formEngine.populateFromSirsi) {
      formEngine.populateFromSirsi(question.Id, org, pageId);
    }
  };

  const handleClear = () => {
    setSearchQuery('');
    setSelectedOrg(null);
    setResults([]);
    onChange(question.Id, '', pageId);
    // Clear auto-populated fields
    if (formEngine.clearSirsiPopulate) {
      formEngine.clearSirsiPopulate(question.Id, pageId);
    }
  };

  // Org type labels for display
  const orgTypeLabels = {
    publicAuthorityCentralGovernment: 'Public authority - central government',
    publicAuthoritySubCentralGovernment: 'Public authority - sub-central government',
    publicUndertaking: 'Public undertaking',
    privateUtility: 'Private utility'
  };

  return (
    <div className="question-group" ref={dropdownRef}>
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      <div style={{position:'relative'}}>
        <div className="ocid-input-group">
          <input
            type="text"
            className={`form-control ${errors?.length ? 'invalid' : ''}`}
            value={searchQuery || value || ''}
            onChange={e => handleSearch(e.target.value)}
            onFocus={() => { if (results.length > 0 && !selectedOrg) setShowDropdown(true); }}
            placeholder="Search by organisation name..."
          />
          {(searchQuery || value) && (
            <button
              className="sd-style"
              onClick={handleClear}
              style={{whiteSpace:'nowrap',padding:'0.375rem 0.75rem'}}
              title="Clear selection"
            >
              Clear
            </button>
          )}
        </div>
        {loading && <div style={{fontSize:'0.8rem',color:'#6c757d',marginTop:'0.25rem'}}>Searching...</div>}
        {showDropdown && (
          <div className="sirsi-dropdown">
            {results.map((org, i) => (
              <div
                key={org.id}
                className="sirsi-dropdown-item"
                onClick={() => handleSelect(org)}
              >
                <div style={{fontWeight:500}}>{org.name}</div>
                <div style={{fontSize:'0.75rem',color:'#6c757d'}}>
                  {orgTypeLabels[org.orgType] || 'Supplier'} | {org.identifiers[0]?.scheme}: {org.identifiers[0]?.id}
                  {org.address?.locality ? ` | ${org.address.locality}` : ''}
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
      {selectedOrg && (
        <div className="ocid-details" style={{marginTop:'0.5rem'}}>
          <div className="ocid-detail-row"><strong>Selected:</strong> {selectedOrg.name}</div>
          <div className="ocid-detail-row"><strong>Type:</strong> {orgTypeLabels[selectedOrg.orgType] || 'N/A'}</div>
          <div className="ocid-detail-row"><strong>Identifier:</strong> {selectedOrg.identifiers[0]?.scheme} - {selectedOrg.identifiers[0]?.id}</div>
          <div className="ocid-detail-row"><strong>Location:</strong> {selectedOrg.address?.locality || 'N/A'}, {selectedOrg.address?.postalCode || ''}</div>
          {selectedOrg.devolvedRegulations && (
            <div className="ocid-detail-row"><strong>Devolved Regs:</strong> {selectedOrg.devolvedRegulations}</div>
          )}
        </div>
      )}
      <ValidationMessage errors={errors} />
    </div>
  );
}

function LookupQuestion({ question, formEngine, pageId }) {
  const items = formEngine.getLookupItems(question, pageId);
  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      {items.length > 0 ? (
        <ul className="lookup-list">
          {items.map((item, i) => <li key={i}>{item.display}</li>)}
        </ul>
      ) : (
        <p style={{color:'#999',fontSize:'0.85rem',fontStyle:'italic'}}>No data available. Enter an OCID to load data.</p>
      )}
    </div>
  );
}

function CPVClassificationQuestion({ question, formEngine, pageId, errors }) {
  const [search, setSearch] = useState('');
  const [selected, setSelected] = useState(new Set());

  // Hydrate from FormEngine on mount (preserves selections across navigation)
  useEffect(() => {
    const scopeKey = pageId ? `${pageId}::${question.Id}` : question.Id;
    // First try selectedOptions (Set of codes), then fall back to answers (array or comma-separated string)
    const opts = formEngine.selectedOptions[scopeKey] || formEngine.selectedOptions[question.Id];
    if (opts && opts.size > 0) {
      setSelected(new Set(opts));
    } else {
      const ans = formEngine.answers[scopeKey] || formEngine.answers[question.Id];
      if (ans) {
        const arr = Array.isArray(ans) ? ans : String(ans).split(',').map(s => s.trim()).filter(Boolean);
        if (arr.length > 0) setSelected(new Set(arr));
      }
    }
  }, []);

  const codes = window.CPV_CODES || [];

  // Build a quick lookup map for code -> description
  const codeMap = useMemo(() => {
    const map = {};
    for (const c of codes) map[c.code] = c.description;
    return map;
  }, [codes]);

  const filtered = useMemo(() => {
    if (!search || search.length < 2) return [];
    const s = search.toLowerCase();
    // Filter matches, prioritising description matches first, then code-only matches
    const descMatches = [];
    const codeOnlyMatches = [];
    for (const c of codes) {
      const descMatch = c.description.toLowerCase().includes(s);
      const codeMatch = c.code.includes(s);
      if (descMatch) descMatches.push(c);
      else if (codeMatch) codeOnlyMatches.push(c);
    }
    return [...descMatches, ...codeOnlyMatches].slice(0, 100);
  }, [search, codes]);

  const toggleCode = (code) => {
    const next = new Set(selected);
    if (next.has(code)) next.delete(code);
    else next.add(code);
    setSelected(next);
    formEngine.setAnswer(question.Id, [...next], pageId);
    // Also store in selectedOptions for consistent hydration and engine access
    const scopeKey = pageId ? `${pageId}::${question.Id}` : question.Id;
    formEngine.selectedOptions[scopeKey] = new Set(next);
    formEngine.selectedOptions[question.Id] = new Set(next);
  };

  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      {selected.size > 0 && (
        <div className="selected-tags">
          {[...selected].map(code => {
            const desc = codeMap[code];
            return (
              <span key={code} className="selected-tag">
                {desc && desc !== code ? `${desc} \u2014 ` : ''}{code}
                <button onClick={() => toggleCode(code)}>&times;</button>
              </span>
            );
          })}
        </div>
      )}
      <div className="picker-container">
        <div className="picker-search">
          <input
            type="text"
            placeholder="Search by description or code (e.g. repair or 50)..."
            value={search}
            onChange={e => setSearch(e.target.value)}
          />
        </div>
        <div className="picker-list">
          {filtered.length === 0 && search.length >= 2 && (
            <div style={{padding:'0.5rem',color:'#999',fontSize:'0.85rem'}}>No results found</div>
          )}
          {filtered.length === 0 && search.length < 2 && (
            <div style={{padding:'0.5rem',color:'#999',fontSize:'0.85rem'}}>Type at least 2 characters to search by description or code</div>
          )}
          {filtered.map(c => (
            <div key={c.code} className="picker-item" onClick={() => toggleCode(c.code)}>
              <input type="checkbox" checked={selected.has(c.code)} readOnly />
              <span className="name">{c.description !== c.code ? c.description : ''}</span>
              <span className="code">{c.code}</span>
            </div>
          ))}
        </div>
      </div>
      <ValidationMessage errors={errors} />
    </div>
  );
}

function RegionQuestion({ question, formEngine, pageId, errors }) {
  const [search, setSearch] = useState('');
  const [selected, setSelected] = useState(new Set());
  const [expanded, setExpanded] = useState(new Set());

  // Hydrate from FormEngine on mount (preserves selections across navigation)
  useEffect(() => {
    const scopeKey = pageId ? `${pageId}::${question.Id}` : question.Id;
    const opts = formEngine.selectedOptions[scopeKey] || formEngine.selectedOptions[question.Id];
    if (opts && opts.size > 0) {
      setSelected(new Set(opts));
      // Auto-expand parent nodes for restored selections
      const restoreExpanded = new Set();
      for (const code of opts) {
        const r = (window.REGIONS || []).find(reg => reg.code === code);
        let parent = r?.parent;
        while (parent) {
          restoreExpanded.add(parent);
          const pr = (window.REGIONS || []).find(reg => reg.code === parent);
          parent = pr?.parent;
        }
      }
      if (restoreExpanded.size > 0) setExpanded(restoreExpanded);
    } else {
      const ans = formEngine.answers[scopeKey] || formEngine.answers[question.Id];
      if (ans) {
        const arr = Array.isArray(ans) ? ans : String(ans).split(',').map(s => s.trim()).filter(Boolean);
        if (arr.length > 0) {
          setSelected(new Set(arr));
          const restoreExpanded = new Set();
          for (const code of arr) {
            const r = (window.REGIONS || []).find(reg => reg.code === code);
            let parent = r?.parent;
            while (parent) {
              restoreExpanded.add(parent);
              const pr = (window.REGIONS || []).find(reg => reg.code === parent);
              parent = pr?.parent;
            }
          }
          if (restoreExpanded.size > 0) setExpanded(restoreExpanded);
        }
      }
    }
  }, []);

  const regions = window.REGIONS || [];

  // Build tree structure (memoized) — UK first since this is a UK procurement form
  const { roots, childrenMap, regionMap, parentChainMap } = useMemo(() => {
    const childrenMap = {};
    const regionMap = {};
    const roots = [];
    for (const r of regions) {
      regionMap[r.code] = r;
      if (r.parent) {
        if (!childrenMap[r.parent]) childrenMap[r.parent] = [];
        childrenMap[r.parent].push(r);
      } else {
        roots.push(r);
      }
    }
    // Sort roots: UK first, then alphabetical by name
    roots.sort((a, b) => {
      if (a.code === 'UK') return -1;
      if (b.code === 'UK') return 1;
      return a.name.localeCompare(b.name);
    });
    // Ancestor chain: code -> [root, ..., parent]
    const parentChainMap = {};
    const getAncestors = (code) => {
      if (parentChainMap[code]) return parentChainMap[code];
      const r = regionMap[code];
      if (!r || !r.parent) { parentChainMap[code] = []; return []; }
      parentChainMap[code] = [...getAncestors(r.parent), r.parent];
      return parentChainMap[code];
    };
    for (const r of regions) getAncestors(r.code);
    return { roots, childrenMap, regionMap, parentChainMap };
  }, [regions]);

  // Search: find matches + compute which ancestors need to be visible/expanded
  const searchState = useMemo(() => {
    if (!search || search.length < 2) return null;
    const s = search.toLowerCase();
    const matchingCodes = new Set();
    const autoExpandCodes = new Set();
    for (const r of regions) {
      if (r.name.toLowerCase().includes(s) || r.code.toLowerCase().includes(s)) {
        matchingCodes.add(r.code);
        // Expand all ancestors so this match is visible
        for (const a of (parentChainMap[r.code] || [])) autoExpandCodes.add(a);
        // Expand the match itself so its children are visible
        if (childrenMap[r.code]?.length > 0) autoExpandCodes.add(r.code);
      }
    }
    return { matchingCodes, autoExpandCodes };
  }, [search, regions, parentChainMap, childrenMap]);

  const isSearchMode = search.length >= 2 && searchState !== null;

  // Get all descendant codes for a given region code
  const getAllDescendants = (code) => {
    const descendants = [];
    const children = childrenMap[code] || [];
    for (const child of children) {
      descendants.push(child.code);
      descendants.push(...getAllDescendants(child.code));
    }
    return descendants;
  };

  // Toggle selection of a single region (no cascading to sub-regions)
  const toggleRegion = (code) => {
    const next = new Set(selected);
    const wasSelected = next.has(code);

    if (wasSelected) {
      next.delete(code);
    } else {
      next.add(code);
    }
    setSelected(next);
    formEngine.setAnswer(question.Id, [...next], pageId);
    // Also store in selectedOptions for consistent hydration and engine access
    const scopeKey = pageId ? `${pageId}::${question.Id}` : question.Id;
    formEngine.selectedOptions[scopeKey] = new Set(next);
    formEngine.selectedOptions[question.Id] = new Set(next);
  };

  const toggleExpand = (code, e) => {
    e.stopPropagation();
    const next = new Set(expanded);
    if (next.has(code)) next.delete(code);
    else next.add(code);
    setExpanded(next);
  };

  // Is a node expanded? In search mode: auto-expanded OR manually expanded. Normal: manually expanded.
  const isNodeExpanded = (code) => {
    if (isSearchMode) {
      return searchState.autoExpandCodes.has(code) || expanded.has(code);
    }
    return expanded.has(code);
  };

  // Recursive tree renderer
  // parentVisible: true if the parent decided to show its children (i.e., parent is expanded)
  const renderNode = (node, depth, parentVisible) => {
    const hasChildren = childrenMap[node.code]?.length > 0;
    const isMatch = isSearchMode && searchState.matchingCodes.has(node.code);
    const isAncestorOfMatch = isSearchMode && searchState.autoExpandCodes.has(node.code);

    // Visibility: In search mode, show if match, ancestor of match, or parent is expanded and showing children
    if (isSearchMode && !isMatch && !isAncestorOfMatch && !parentVisible) {
      return null;
    }

    const nodeExpanded = isNodeExpanded(node.code);

    return (
      <React.Fragment key={node.code}>
        <div
          className={`region-tree-row ${selected.has(node.code) ? 'region-selected' : ''} ${isMatch ? 'region-match' : ''}`}
          onClick={() => toggleRegion(node.code)}
        >
          <div className="region-indent" style={{width: (depth * 24) + 'px'}} />
          {hasChildren ? (
            <span className="region-chevron" onClick={(e) => toggleExpand(node.code, e)}>
              {nodeExpanded ? '\u25BC' : '\u25B6'}
            </span>
          ) : (
            <span className="region-chevron-space" />
          )}
          <input type="checkbox" checked={selected.has(node.code)} readOnly className="region-checkbox" />
          <span className="region-name">{node.name}</span>
          <span className="region-code">{node.code}</span>
        </div>
        {hasChildren && nodeExpanded && childrenMap[node.code].map(child =>
          renderNode(child, depth + 1, true)
        )}
      </React.Fragment>
    );
  };

  const hasSearchResults = searchState && searchState.matchingCodes.size > 0;

  return (
    <div className="question-group">
      <label className="question-label">{question.Label}</label>
      {question.Hint?.Text && <div className="question-hint">{question.Hint.Text}</div>}
      {selected.size > 0 && (
        <div className="selected-tags">
          {[...selected].map(code => {
            const r = regionMap[code];
            return (
              <span key={code} className="selected-tag">
                {r ? r.name : code} ({code})
                <button onClick={() => toggleRegion(code)}>&times;</button>
              </span>
            );
          })}
        </div>
      )}
      <div className="region-picker">
        <div className="region-search">
          <input
            type="text"
            placeholder="Search by region name or code..."
            value={search}
            onChange={e => setSearch(e.target.value)}
          />
        </div>
        <div className="region-tree">
          {isSearchMode && !hasSearchResults && (
            <div className="region-empty">No regions found</div>
          )}
          {roots.map(r => renderNode(r, 0, false))}
        </div>
      </div>
      <ValidationMessage errors={errors} />
    </div>
  );
}

// ============================================================
// QUESTION RENDERER - Routes to specific component
// ============================================================
function QuestionRenderer({ question, formEngine, pageId, version }) {
  const isVisible = formEngine.isQuestionVisible(question, pageId);
  if (!isVisible) return null;

  const qId = question.Id;
  const scopedKey = `${pageId}::${qId}`;
  // Use scoped values only — prevents cross-instance data leakage on repeating/nested pages.
  // Defaults are pre-stored with scoped keys in applyDefaults().
  // Global (bare qId) is only used by engines for cross-page visibility references.
  const value = formEngine.answers[scopedKey] || '';
  const selectedOpts = formEngine.selectedOptions[scopedKey] || new Set();
  const errors = formEngine.getErrors(qId, pageId);
  const warnings = formEngine.getWarnings(qId, pageId);
  const options = formEngine.getOptionsForQuestion(question, pageId);

  const handleChange = (id, val, pid) => {
    formEngine.setAnswer(id, val, pid);
  };

  const handleOptionChange = (id, xmlValue, sel, pid, isExcl, qType) => {
    formEngine.setOption(id, xmlValue, sel, pid, isExcl, qType);
  };

  const type = question.Type;

  switch (type) {
    case 'Text':
      return <TextQuestion question={question} value={value} onChange={handleChange} errors={errors} pageId={pageId} />;
    case 'MultiLineText':
      return <MultiLineTextQuestion question={question} value={value} onChange={handleChange} errors={errors} pageId={pageId} />;
    case 'Radio':
      return <RadioQuestion question={question} options={options} selectedOptions={selectedOpts} onOptionChange={handleOptionChange} errors={errors} pageId={pageId} formEngine={formEngine} />;
    case 'Checkbox':
      return <CheckboxQuestion question={question} options={options} selectedOptions={selectedOpts} onOptionChange={handleOptionChange} errors={errors} pageId={pageId} formEngine={formEngine} />;
    case 'Dropdown':
      return <DropdownQuestion question={question} options={options} selectedOptions={selectedOpts} onOptionChange={handleOptionChange} errors={errors} pageId={pageId} />;
    case 'Date':
    case 'DateTime':
      return <DateQuestion question={question} value={value} onChange={handleChange} errors={errors} pageId={pageId} />;
    case 'Label':
      return <LabelQuestion question={question} formEngine={formEngine} pageId={pageId} />;
    case 'OCID':
      return <OcidQuestion question={question} value={value} onChange={handleChange} errors={errors} warnings={warnings} pageId={pageId} formEngine={formEngine} />;
    case 'SirsiSearch':
      return <SirsiSearchQuestion question={question} value={value} onChange={handleChange} errors={errors} pageId={pageId} formEngine={formEngine} />;
    case 'Lookup':
      return <LookupQuestion question={question} formEngine={formEngine} pageId={pageId} />;
    case 'CPVClassification':
      return <CPVClassificationQuestion question={question} formEngine={formEngine} pageId={pageId} errors={errors} />;
    case 'Region':
      return <RegionQuestion question={question} formEngine={formEngine} pageId={pageId} errors={errors} />;
    default:
      return (
        <div className="question-group">
          <div className="alert alert-warning" style={{fontSize:'0.85rem'}}>
            Unknown question type: <strong>{type}</strong> (ID: {qId})
          </div>
        </div>
      );
  }
}

// ============================================================
// PAGE RENDERER
// ============================================================
function PageRenderer({ page, formEngine, version }) {
  if (!page) return null;

  const visibleQuestions = page.Questions.filter(q =>
    formEngine.isQuestionVisible(q, page.instanceId)
  );

  return (
    <div>
      <div className="page-header">
        <h2>{formEngine.getPageDisplayTitle(page)}</h2>
      </div>
      {visibleQuestions.map(q => (
        <QuestionRenderer
          key={`${page.instanceId}_${q.Id}`}
          question={q}
          formEngine={formEngine}
          pageId={page.instanceId}
          version={version}
        />
      ))}
      {visibleQuestions.length === 0 && (
        <p style={{color:'#999',fontStyle:'italic'}}>No questions visible on this page with current selections.</p>
      )}
    </div>
  );
}

// ============================================================
// SIDEBAR
// ============================================================
function Sidebar({ pages, currentIndex, onNavigate, formId, formEngine }) {
  return (
    <div className="sticky-nav">
      <nav className="sd-style">
        <div style={{fontWeight:600,fontSize:'0.9rem',marginBottom:'0.75rem',paddingBottom:'0.5rem',borderBottom:'1px solid #eee'}}>
          {formId} Pages
        </div>
        <div className="scrollable-nav">
          {pages.map((page, idx) => (
            <button
              key={page.instanceId}
              className={`sidebar-nav-item ${idx === currentIndex ? 'active' : ''} ${page.isNestedPage ? 'nested' : ''}`}
              onClick={() => onNavigate(idx)}
            >
              {page.isNestedPage ? '\u2514 ' : ''}{formEngine ? formEngine.getPageDisplayTitle(page) : getDisplayTitle(page, pages)}
            </button>
          ))}
        </div>
      </nav>
    </div>
  );
}

function getDisplayTitle(page, allPages) {
  if (page.optionLabel) return `${page.Title} - ${page.optionLabel}`;
  const siblings = allPages.filter(p => p.Id === page.Id);
  if (siblings.length > 1) {
    const myIdx = siblings.indexOf(page);
    return `${page.Title} (${myIdx + 1})`;
  }
  return page.Title;
}

// ============================================================
// XML PREVIEW
// ============================================================
function XmlPreview({ xml }) {
  const [show, setShow] = useState(false);
  if (!xml) return null;
  return (
    <div className="xml-preview">
      <button className="sd-style" onClick={() => setShow(!show)} style={{marginBottom:'0.5rem'}}>
        {show ? 'Hide' : 'Show'} XML Preview
      </button>
      {show && <pre>{xml}</pre>}
    </div>
  );
}

// ============================================================
// PAGE NAV BUTTONS
// ============================================================
function PageNavButtons({ currentIndex, totalPages, onPrev, onNext, onGenerate, onSave, onPublish, onLoadDraft, status, actionLoading }) {
  const hasJwt = !!(window.APP_CONFIG?.jwt);
  const hasApiUrl = !!(window.APP_CONFIG?.apiUrl);
  const canPublish = hasJwt && hasApiUrl;

  return (
    <div className="page-nav-buttons">
      <button
        className="sd-style"
        onClick={onPrev}
        disabled={currentIndex === 0}
      >
        &larr; Previous
      </button>
      <button
        className="sd-style"
        onClick={onNext}
        disabled={currentIndex >= totalPages - 1}
      >
        Next &rarr;
      </button>
      <div className="spacer" />
      {canPublish && (
        <button
          className="sd-style"
          onClick={onLoadDraft}
          title="Load saved draft from SourceDogg"
          disabled={actionLoading === 'load'}
          style={{opacity: actionLoading === 'load' ? 0.6 : 1}}
        >
          {actionLoading === 'load' ? 'Loading...' : 'Load Draft'}
        </button>
      )}
      <button
        className="sd-style"
        onClick={onGenerate}
        style={{fontWeight:600}}
      >
        Generate XML
      </button>
      {canPublish && (
        <button
          className="sd-style save-btn"
          onClick={onSave}
          title="Save draft to SourceDogg"
          disabled={actionLoading === 'save'}
        >
          {actionLoading === 'save' ? 'Saving...' : 'Save Draft'}
        </button>
      )}
      {canPublish && (
        <button
          className="sd-style publish-btn"
          onClick={onPublish}
          title="Publish XML to gov portal"
          disabled={actionLoading === 'publish'}
        >
          {actionLoading === 'publish' ? 'Publishing...' : 'Publish'}
        </button>
      )}
      {status && (
        <span className={`status-message ${status.type}`}>{status.message}</span>
      )}
    </div>
  );
}

// ============================================================
// FORM SHELL - Main form container
// ============================================================
function FormShell({ formId }) {
  const [loading, setLoading] = useState(true);
  const [version, setVersion] = useState(0);
  const [currentPageIndex, setCurrentPageIndex] = useState(0);
  const [xmlOutput, setXmlOutput] = useState(null);
  const [status, setStatus] = useState(null);
  const [publishResult, setPublishResult] = useState(null);
  const [actionLoading, setActionLoading] = useState(null); // 'save' | 'publish' | null
  const formEngineRef = useRef(null);

  useEffect(() => {
    const loadData = async () => {
      // Load form JSON if not already loaded
      if (formId === 'UK6' && !window.UK6_DATA) {
        const resp = await fetch('/src/data/UK6.json');
        window.UK6_DATA = await resp.json();
      }
      if (formId === 'UK12' && !window.UK12_DATA) {
        const resp = await fetch('/src/data/UK12.json');
        window.UK12_DATA = await resp.json();
      }

      const engine = window.FormEngine.create();
      engine.loadForm(formId);

      // Wrap setAnswer/setOption to trigger re-render
      const origSetAnswer = engine.setAnswer.bind(engine);
      engine.setAnswer = (...args) => {
        origSetAnswer(...args);
        setVersion(v => v + 1);
      };
      const origSetOption = engine.setOption.bind(engine);
      engine.setOption = (...args) => {
        origSetOption(...args);
        setVersion(v => v + 1);
      };
      const origSetOcid = engine.setOcidData.bind(engine);
      engine.setOcidData = (...args) => {
        const result = origSetOcid(...args);
        setVersion(v => v + 1);
        return result;
      };
      const origEvalAll = engine.evaluateAll.bind(engine);
      engine.evaluateAll = (...args) => {
        origEvalAll(...args);
        // Don't trigger re-render here to avoid infinite loops
      };
      // Wrap SIRSI populate to trigger re-render
      const origPopSirsi = engine.populateFromSirsi.bind(engine);
      engine.populateFromSirsi = (...args) => {
        origPopSirsi(...args);
        setVersion(v => v + 1);
      };
      const origClearSirsi = engine.clearSirsiPopulate.bind(engine);
      engine.clearSirsiPopulate = (...args) => {
        origClearSirsi(...args);
        setVersion(v => v + 1);
      };

      formEngineRef.current = engine;
      setLoading(false);
      setVersion(1);

      // Auto-load draft from SourceDogg if JWT is available (mirrors Blazor behavior)
      const { jwt, apiUrl } = window.APP_CONFIG;
      if (jwt && apiUrl) {
        setStatus({ type: 'info', message: 'Loading draft from SourceDogg...' });
        setActionLoading('load');
        try {
          const result = await window.ApiService.loadDraft(jwt, apiUrl);
          if (!result.error) {
            if (result.formCode && result.formCode !== formId) {
              console.warn(`[AutoLoad] Draft is ${result.formCode}, current form is ${formId} — skipping restore`);
              setStatus({ type: 'info', message: `Draft is ${result.formCode} (current form: ${formId}). Skipped auto-load.` });
            } else {
              engine.restoreFromSourceDogg(result);
              setVersion(v => v + 1);
              const totalAnswers = result.state ? Object.values(result.state).reduce((sum, page) =>
                sum + Object.values(page).filter(v => v).length, 0) : 0;
              setStatus({
                type: 'success',
                message: `Draft loaded! Form: ${result.formCode}, ${totalAnswers} answers restored.`
              });
            }
          } else {
            console.warn('[AutoLoad]', result.message);
            setStatus(null); // Don't show error for auto-load — user can still use the form
          }
        } catch (err) {
          console.warn('[AutoLoad] Error:', err);
          setStatus(null);
        }
        setActionLoading(null);
      }
    };

    loadData();
  }, [formId]);

  const formEngine = formEngineRef.current;
  if (loading || !formEngine) {
    return (
      <div className="loading">
        <div className="spinner" />
        Loading {formId} form...
      </div>
    );
  }

  const visiblePages = formEngine.visiblePages || [];
  const currentPage = visiblePages[currentPageIndex] || null;

  const handleNavigate = (idx) => {
    // Auto-save current page to SourceDogg on navigation (mirrors Blazor behavior)
    const { jwt, apiUrl } = window.APP_CONFIG;
    if (jwt && apiUrl && formEngine) {
      const state = window.ApiService.collectFormState(formEngine);
      window.ApiService.saveDraft(jwt, apiUrl, state).then(result => {
        if (result.error) console.warn('[AutoSave]', result.message);
        else console.log('[AutoSave] Draft saved on page navigation');
      }).catch(err => console.warn('[AutoSave] Error:', err));
    }
    setCurrentPageIndex(idx);
    setStatus(null);
    window.scrollTo(0, 0);
  };

  const handlePrev = () => {
    if (currentPageIndex > 0) handleNavigate(currentPageIndex - 1);
  };

  const handleNext = () => {
    if (currentPageIndex < visiblePages.length - 1) handleNavigate(currentPageIndex + 1);
  };

  const handleGenerateXml = () => {
    try {
      const xml = formEngine.downloadXml();
      setXmlOutput(xml);
      setStatus({ type: 'success', message: 'XML generated and downloaded!' });
      setPublishResult(null);
    } catch (err) {
      console.error('XML generation error:', err);
      setStatus({ type: 'error', message: 'Error generating XML: ' + err.message });
    }
  };

  const handleLoadDraft = async () => {
    const { jwt, apiUrl } = window.APP_CONFIG;
    if (!jwt || !apiUrl) {
      setStatus({ type: 'error', message: 'JWT and API URL required for Load' });
      return;
    }
    setActionLoading('load');
    setStatus({ type: 'info', message: 'Loading draft from SourceDogg...' });
    try {
      const result = await window.ApiService.loadDraft(jwt, apiUrl);
      if (result.error) {
        setStatus({ type: 'error', message: result.message });
      } else if (result.formCode && result.formCode !== formId) {
        setStatus({ type: 'error', message: `Draft is ${result.formCode} but current form is ${formId}. Cannot restore cross-form data.` });
      } else {
        // Restore state into the form engine
        formEngine.restoreFromSourceDogg(result);
        setVersion(v => v + 1);

        const totalAnswers = result.state ? Object.values(result.state).reduce((sum, page) =>
          sum + Object.values(page).filter(v => v).length, 0) : 0;
        setStatus({
          type: 'success',
          message: `Draft loaded! Form: ${result.formCode}, ${totalAnswers} answers restored.`
        });
      }
    } catch (err) {
      setStatus({ type: 'error', message: 'Load error: ' + err.message });
    }
    setActionLoading(null);
  };

  const handleSave = async () => {
    const { jwt, apiUrl } = window.APP_CONFIG;
    if (!jwt || !apiUrl) {
      setStatus({ type: 'error', message: 'JWT and API URL required for Save' });
      return;
    }
    setActionLoading('save');
    setStatus({ type: 'info', message: 'Saving draft...' });
    const state = window.ApiService.collectFormState(formEngine);
    const result = await window.ApiService.saveDraft(jwt, apiUrl, state);
    setActionLoading(null);
    setStatus({
      type: result.error ? 'error' : 'success',
      message: result.message
    });
  };

  const handlePublish = async () => {
    const { jwt, apiUrl } = window.APP_CONFIG;
    if (!jwt || !apiUrl) {
      setStatus({ type: 'error', message: 'JWT and API URL required for Publish' });
      return;
    }

    // ── Validation gate: run all validation rules and block if errors ──
    formEngine.updateValidation();
    const allErrors = [];
    for (const page of formEngine.visiblePages) {
      for (const q of page.Questions) {
        if (!formEngine.isQuestionVisible(q, page.instanceId)) continue;
        const errors = formEngine.getErrors(q.Id, page.instanceId);
        if (errors.length > 0) {
          allErrors.push({
            page: page.Label || page.Id,
            pageId: page.instanceId,
            question: q.Label || q.Id,
            questionId: q.Id,
            errors
          });
        }
      }
    }

    if (allErrors.length > 0) {
      // Show validation errors and block publish
      const errorSummary = allErrors.map(e =>
        `${e.page} > ${e.question}: ${e.errors.join('; ')}`
      );
      setStatus({ type: 'error', message: `Cannot publish — ${allErrors.length} field(s) have validation errors.` });
      setPublishResult({
        error: true,
        message: 'Form Validation Failed',
        validationErrors: errorSummary,
        rawError: null
      });
      // Navigate to the first page with errors
      const firstErrorPageId = allErrors[0].pageId;
      const errPageIdx = (formEngine.visiblePages || []).findIndex(p => p.instanceId === firstErrorPageId);
      if (errPageIdx >= 0) {
        setCurrentPageIndex(errPageIdx);
      }
      setVersion(v => v + 1); // Re-render to show inline errors
      return;
    }

    setActionLoading('publish');
    setStatus({ type: 'info', message: 'Generating XML and publishing...' });
    setPublishResult(null);

    try {
      // Generate XML
      const xml = window.XmlBuilder.build(formEngine);
      setXmlOutput(xml);

      // Publish to gov portal
      const result = await window.ApiService.publishXml(jwt, apiUrl, xml);
      setActionLoading(null);

      if (result.error) {
        setStatus({ type: 'error', message: result.message });
        setPublishResult(result);
      } else {
        setStatus({ type: 'success', message: result.message });
        setPublishResult(result);
      }
    } catch (err) {
      setActionLoading(null);
      setStatus({ type: 'error', message: 'Error: ' + err.message });
    }
  };

  return (
    <div>
      {/* Header */}
      <div className="nav-header">
        <span className="title">Find a Tender Forms</span>
        <div style={{display:'flex',alignItems:'center',gap:'1rem'}}>
          <span className="env-badge">{window.APP_CONFIG?.env === 'prod' ? 'PROD' : 'TPP'}</span>
          <span className="form-id-badge">{formId}</span>
          <a href="#/" style={{fontSize:'0.85rem'}}>Change form</a>
        </div>
      </div>

      {/* Main layout */}
      <div className="container-fluid" style={{marginTop:'1rem'}}>
        <div className="row">
          {/* Sidebar */}
          <div className="col-12 col-md-3 col-lg-2 sidebar-column">
            <Sidebar
              pages={visiblePages}
              currentIndex={currentPageIndex}
              onNavigate={handleNavigate}
              formId={formId}
              formEngine={formEngine}
            />
          </div>

          {/* Main content */}
          <div className="col">
            <div className="main-content">
              <PageRenderer
                page={currentPage}
                formEngine={formEngine}
                version={version}
              />
              <PageNavButtons
                currentIndex={currentPageIndex}
                totalPages={visiblePages.length}
                onPrev={handlePrev}
                onNext={handleNext}
                onGenerate={handleGenerateXml}
                onSave={handleSave}
                onPublish={handlePublish}
                onLoadDraft={handleLoadDraft}
                status={status}
                actionLoading={actionLoading}
              />

              {/* Publish Result Panel */}
              {publishResult && (
                <div className={`publish-result ${publishResult.error ? 'error' : 'success'}`}>
                  <h4>{publishResult.error ? 'Publication Failed' : 'Published Successfully!'}</h4>
                  {publishResult.validationErrors && publishResult.validationErrors.length > 0 && (
                    <div className="validation-errors-list">
                      <h5>Gov Portal Validation Errors:</h5>
                      <ul>
                        {publishResult.validationErrors.map((err, i) => (
                          <li key={i}>{err}</li>
                        ))}
                      </ul>
                    </div>
                  )}
                  {publishResult.rawError && (
                    <details style={{marginTop:'0.5rem'}}>
                      <summary style={{cursor:'pointer',fontSize:'0.8rem',opacity:0.7}}>Raw error response</summary>
                      <pre style={{fontSize:'0.7rem',maxHeight:'200px',overflow:'auto',marginTop:'0.5rem'}}>
                        {publishResult.rawError}
                      </pre>
                    </details>
                  )}
                </div>
              )}

              {actionLoading && (
                <div className="action-loading">
                  <div className="spinner" />
                  {actionLoading === 'save' ? 'Saving draft...' : 'Publishing to gov portal...'}
                </div>
              )}

              <XmlPreview xml={xmlOutput} />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============================================================
// APP - Root component with hash routing
// ============================================================
function App() {
  const [route, setRoute] = useState(window.location.hash || '#/');

  useEffect(() => {
    const handleHashChange = () => setRoute(window.location.hash || '#/');
    window.addEventListener('hashchange', handleHashChange);
    return () => window.removeEventListener('hashchange', handleHashChange);
  }, []);

  // Parse route
  const formMatch = route.match(/#\/form\/(\w+)/);
  if (formMatch) {
    return <FormShell formId={formMatch[1]} key={formMatch[1]} />;
  }

  const handleFormSelect = (formId) => {
    window.location.hash = `#/form/${formId}`;
  };

  return <SetupPanel onContinue={handleFormSelect} />;
}

// ============================================================
// RENDER
// ============================================================
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
