import React, { useState } from 'react';
import { useEditor } from '@craftjs/core';
import { storeWebhookSecret } from '../../../utils/form-webhook-secret';
import {
  BG_COLORS,
  SPACING_PRESETS,
  RADIUS_PRESETS,
  SHADOW_PRESETS,
} from '../../../constants/presets';
import {
  StylePanelProps,
  SectionLabel,
  ColorSwatchGrid,
  PresetButtonGrid,
  CollapsibleSection,
  ArrayPropEditor,
  SpacingControl,
  BorderControl,
  BorderValue,
  AnimationControl,
  VisibilityControl,
  buildBorderShorthand,
  labelStyle,
  inputStyle,
  smallInputStyle,
  btnActiveStyle,
  sectionGap,
  useNodeProp,
} from './shared';

/* The full sanitizeInputType (utils/escape.ts) allowlist, plus the two
   fake "types" (textarea/select) that take their own ContactForm render
   branch instead of an <input type>. Kept as a local list (rather than
   importing the runtime array from utils/escape.ts) since this is
   presentation-only -- the actual security boundary is enforced in
   ContactForm.toHtml via sanitizeInputType, not here. */
const CONTACT_FIELD_TYPES = [
  'text', 'email', 'tel', 'number', 'password', 'url', 'search', 'date',
  'checkbox', 'radio', 'textarea', 'select',
];

const moveBtnStyle: React.CSSProperties = {
  flex: 1, padding: '3px 6px', fontSize: 10, background: '#27272a', color: '#a1a1aa',
  border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer',
};

function parseBorderValue(v: unknown): BorderValue {
  const s = typeof v === 'string' ? v.trim() : '';
  if (!s || s === 'none') return { width: '', style: 'none', color: '' };
  const m = s.match(/^(\S+)\s+(\S+)\s+(.+)$/);
  if (!m) return { width: '', style: 'none', color: '' };
  return { width: m[1], style: m[2], color: m[3] };
}

const SPACING_SIDE_KEYS: { side: 'top' | 'right' | 'bottom' | 'left'; suffix: 'Top' | 'Right' | 'Bottom' | 'Left' }[] = [
  { side: 'top', suffix: 'Top' },
  { side: 'right', suffix: 'Right' },
  { side: 'bottom', suffix: 'Bottom' },
  { side: 'left', suffix: 'Left' },
];

const hintStyle: React.CSSProperties = { fontSize: 10, color: '#71717a', margin: '4px 0 0' };

/**
 * Does this look like a webhook URL the publish step will accept?
 *
 * Mirrors the shape `FormRelayProvisioner::upsertWebhookToken()` enforces
 * (absolute https, a host, no whitespace/control characters) closely enough to
 * warn in the panel. It is a HINT, not a gate -- the server-side check is the
 * real one, and this deliberately never edits or blocks the value.
 */
export function isHttpsWebhookUrl(value: unknown): boolean {
  const v = typeof value === 'string' ? value.trim() : '';
  if (v === '' || /[\s\x00-\x1F\x7F]/.test(v)) return false;
  try {
    const u = new URL(v);
    return u.protocol === 'https:' && u.hostname !== '';
  } catch {
    return false;
  }
}

/* ---------- Webhook shared secret: WRITE-ONLY field ----------
   The raw secret lives in this component's local state and nowhere else. On
   blur it is POSTed to the panel endpoint, which returns an opaque id; only
   that id is handed to `onStored` (and thence to a craft prop). The field is
   then cleared, because there is no read route and nothing to show back --
   the UI offers set / replace / clear, never "view".

   MOUNT THIS WITH `key={selectedId}`. GuidedStyles renders <FormStylePanel>
   with no key, so a selection change re-renders this component rather than
   remounting it, and React keeps `draft`/`status`. A failed store deliberately
   RETAINS the draft (so a 429 or a network blip doesn't make the customer
   retype a pasted key) -- which means without the key, clicking a second
   contact form shows node A's raw secret in node B's field, and the next blur
   assigns the returned id to the wrong form and burns a slot against the
   per-site cap. The `status` banner leaks the same way. */
export const WebhookSecretField: React.FC<{
  secretId: string;
  onStored: (secretId: string) => void;
  onRemoved: () => void;
}> = ({ secretId, onStored, onRemoved }) => {
  const [draft, setDraft] = useState('');
  const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
  const [error, setError] = useState('');

  const save = async () => {
    const raw = draft.trim();
    if (raw === '' || status === 'saving') return;
    setStatus('saving');
    setError('');
    const result = await storeWebhookSecret(raw);
    if (result.ok && result.secretId) {
      // Only the id crosses this line. The raw value is dropped here and is
      // never written to a prop, to storage, or back into the input.
      onStored(result.secretId);
      setDraft('');
      setStatus('saved');
    } else {
      setStatus('error');
      setError(result.error || 'Could not store the secret.');
    }
  };

  return (
    <div style={sectionGap}>
      <label style={labelStyle} htmlFor="whp-webhook-secret">Shared secret (optional)</label>
      <input
        id="whp-webhook-secret"
        data-testid="webhook-secret-input"
        type="password"
        autoComplete="new-password"
        value={draft}
        onChange={(e) => { setDraft(e.target.value); if (status !== 'idle') { setStatus('idle'); setError(''); } }}
        onBlur={() => { void save(); }}
        placeholder={secretId ? 'Paste a new secret to replace' : 'Paste the secret from your receiver'}
        style={inputStyle}
      />
      {secretId && (
        <>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 4 }}>
            <span data-testid="webhook-secret-status" style={{ fontSize: 10, color: '#22c55e' }}>Secret stored</span>
            {/* "Clear", not "Remove": this only drops the form's reference to the
                key. The stored key file stays on the server and still counts
                toward the per-site cap -- nothing deletes one, so a button
                labelled Remove would be telling the customer they had reclaimed
                a slot right up until the 429 that says otherwise. */}
            <button
              data-testid="webhook-secret-remove"
              onClick={onRemoved}
              style={{ ...moveBtnStyle, flex: 'none', padding: '3px 8px' }}
            >
              Clear
            </button>
          </div>
          <p style={hintStyle}>
            Clearing stops this form using the secret; the stored key stays on the server.
          </p>
        </>
      )}
      {status === 'saving' && <p style={hintStyle}>Storing…</p>}
      {status === 'saved' && <p data-testid="webhook-secret-saved" style={{ ...hintStyle, color: '#22c55e' }}>Secret stored.</p>}
      {status === 'error' && <p data-testid="webhook-secret-error" style={{ ...hintStyle, color: '#f87171' }}>{error}</p>}
      <p style={hintStyle}>
        Stored on the server and never shown again — paste a new one to replace it. Used to sign
        (or authorise) each delivery so your receiver can verify it came from this site.
      </p>
    </div>
  );
};

/* ---------- FORM ---------- */
export const FormStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
  const { actions } = useEditor();
  const { setProp, setPropStyle } = useNodeProp(selectedId);

  const style = nodeProps.style || {};
  // ContactForm only: FormContainer/SubscribeForm have no destinationType prop,
  // so their relay controls stay exactly as they were.
  const isWebhook = nodeProps.destinationType === 'webhook';

  const updateField = (index: number, patch: Record<string, any>) => {
    actions.setProp(selectedId, (props: any) => {
      const updated = [...(props.fields || [])];
      updated[index] = { ...updated[index], ...patch };
      props.fields = updated;
    });
  };

  const moveField = (index: number, direction: -1 | 1) => {
    actions.setProp(selectedId, (props: any) => {
      const updated = [...(props.fields || [])];
      const newIndex = index + direction;
      if (newIndex < 0 || newIndex >= updated.length) return;
      [updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
      props.fields = updated;
    });
  };

  return (
    <>
      {/* ContactForm field editor: add/remove/reorder fields, set each
          field's label, name, type (full allowlist + textarea/select),
          options (select only), and required flag. */}
      {nodeProps.fields !== undefined && Array.isArray(nodeProps.fields) && (
        <CollapsibleSection title="Fields">
          <ArrayPropEditor
            selectedId={selectedId}
            propKey="fields"
            items={nodeProps.fields}
            renderItem={(item: any, index: number) => (
              <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <input
                  type="text"
                  value={item.label || ''}
                  onChange={(e) => updateField(index, { label: e.target.value })}
                  placeholder="Label"
                  style={smallInputStyle}
                />
                <input
                  type="text"
                  value={item.name || ''}
                  onChange={(e) => updateField(index, { name: e.target.value })}
                  placeholder="Field name (e.g. email)"
                  style={smallInputStyle}
                />
                <input
                  type="text"
                  value={item.placeholder || ''}
                  onChange={(e) => updateField(index, { placeholder: e.target.value })}
                  placeholder="Placeholder"
                  style={smallInputStyle}
                />
                <select
                  value={item.type || 'text'}
                  onChange={(e) => updateField(index, { type: e.target.value })}
                  style={{ ...smallInputStyle, cursor: 'pointer' }}
                >
                  {CONTACT_FIELD_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
                </select>
                {item.type === 'select' && (
                  <input
                    type="text"
                    value={(item.options || []).join(', ')}
                    onChange={(e) => updateField(index, {
                      options: e.target.value.split(',').map((s: string) => s.trim()).filter(Boolean),
                    })}
                    placeholder="Options (comma-separated)"
                    style={smallInputStyle}
                  />
                )}
                <label style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: '#e4e4e7', cursor: 'pointer' }}>
                  <input
                    type="checkbox"
                    checked={!!item.required}
                    onChange={(e) => updateField(index, { required: e.target.checked })}
                  />
                  Required
                </label>
                <div style={{ display: 'flex', gap: 4 }}>
                  <button disabled={index === 0} onClick={() => moveField(index, -1)} style={{ ...moveBtnStyle, opacity: index === 0 ? 0.4 : 1 }} title="Move up">
                    <i className="fa fa-arrow-up" />
                  </button>
                  <button disabled={index === nodeProps.fields.length - 1} onClick={() => moveField(index, 1)} style={{ ...moveBtnStyle, opacity: index === nodeProps.fields.length - 1 ? 0.4 : 1 }} title="Move down">
                    <i className="fa fa-arrow-down" />
                  </button>
                </div>
              </div>
            )}
            emptyItem={{ type: 'text', label: 'New Field', name: 'field', placeholder: '', required: false }}
          />
        </CollapsibleSection>
      )}

      {/* Destination: email (default, unchanged behaviour) or webhook. Gated on
          the `destinationType` default in ContactForm.craft.props -- a prop
          missing from those defaults is `undefined` here and the control would
          simply never render. */}
      {nodeProps.destinationType !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Send submissions to</label>
          <div style={{ display: 'flex', gap: 4 }}>
            {[{ v: 'email', l: 'Email' }, { v: 'webhook', l: 'Webhook' }].map((o) => (
              <button
                key={o.v}
                data-testid={`destination-${o.v}`}
                onClick={() => setProp('destinationType', o.v)}
                style={btnActiveStyle((nodeProps.destinationType || 'email') === o.v)}
              >
                {o.l}
              </button>
            ))}
          </div>
        </div>
      )}

      {nodeProps.destinationType !== undefined && isWebhook && (
        <>
          <div style={sectionGap}>
            <label style={labelStyle}>Webhook URL</label>
            <input
              type="text"
              data-testid="webhook-url"
              value={nodeProps.webhookUrl || ''}
              onChange={(e) => setProp('webhookUrl', e.target.value)}
              placeholder="https://hooks.example.com/..."
              style={inputStyle}
            />
            {/* The publish step DOES refuse a blank/non-https URL -- loudly, but
                into an error_log the customer never reads, leaving them with a
                form that just doesn't work. Warn here instead. Deliberately a
                warning only: not blanking the value and not blocking the
                publish, since either would trade a loud server-side refusal for
                a silently inert form. */}
            {!isHttpsWebhookUrl(nodeProps.webhookUrl) && (
              <p data-testid="webhook-url-warning" style={{ ...hintStyle, color: '#fbbf24' }}>
                {nodeProps.webhookUrl
                  ? 'This must be an absolute https:// URL — submissions to this form won\'t be delivered until it is.'
                  : 'Enter the https:// URL to POST submissions to — this form won\'t deliver anything until you do.'}
              </p>
            )}
            <p style={hintStyle}>
              Must be an absolute <strong>https://</strong> URL. Each submission is POSTed as JSON;
              failures are retried, then emailed to the fallback address below.
            </p>
          </div>
          <div style={sectionGap}>
            <label style={labelStyle}>Authentication</label>
            <select
              data-testid="webhook-authmode"
              value={nodeProps.webhookAuthMode || 'signature'}
              onChange={(e) => setProp('webhookAuthMode', e.target.value)}
              style={{ ...inputStyle, cursor: 'pointer' }}
            >
              <option value="signature">Signature (HMAC-SHA256 header)</option>
              <option value="bearer">Bearer token (Authorization header)</option>
            </select>
          </div>
          <WebhookSecretField
            /* Remounts on every selection change, so a retained draft (and its
               status banner) can never follow the customer to another form --
               see the comment on WebhookSecretField. */
            key={selectedId}
            secretId={nodeProps.webhookSecretId || ''}
            onStored={(id) => setProp('webhookSecretId', id)}
            onRemoved={() => setProp('webhookSecretId', '')}
          />
        </>
      )}

      {/* Contact-form relay: where submissions are emailed. Present on ContactForm
          and FormContainer (both have recipientEmail/thankYouUrl props). With a
          webhook destination this same address is the FALLBACK the relay uses
          when delivery is exhausted. */}
      {nodeProps.recipientEmail !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>{isWebhook ? 'Fallback email (if the webhook fails)' : 'Send submissions to (email)'}</label>
          <input type="email" value={nodeProps.recipientEmail || ''} onChange={(e) => setProp('recipientEmail', e.target.value)} placeholder="you@example.com" style={inputStyle} />
          <p style={hintStyle}>
            {isWebhook
              ? 'Emailed here if the webhook keeps failing after retries. Leave blank to skip the fallback.'
              : "Emailed via the site's contact-form relay (an admin must enable it in Server Settings). Leave blank to use the Form Action URL instead."}
          </p>
        </div>
      )}
      {nodeProps.thankYouUrl !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Thank-you page URL (optional)</label>
          <input type="text" value={nodeProps.thankYouUrl || ''} onChange={(e) => setProp('thankYouUrl', e.target.value)} placeholder="/thank-you (blank = hosted page)" style={inputStyle} />
        </div>
      )}

      {/* Form action/method (FormContainer). SearchBar also has an `action`
          prop but is distinguished via its unique `showButton` prop -- see
          the dedicated Search block below -- so it doesn't get this label. */}
      {nodeProps.action !== undefined && nodeProps.showButton === undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Form Action URL</label>
          <input type="text" value={nodeProps.action || ''} onChange={(e) => setProp('action', e.target.value)} placeholder="https://..." style={inputStyle} />
        </div>
      )}

      {/* SearchBar: where the GET search request is submitted. */}
      {nodeProps.showButton !== undefined && nodeProps.action !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Search Results Page</label>
          <input type="text" value={nodeProps.action || ''} onChange={(e) => setProp('action', e.target.value)} placeholder="/ (site root) or /search" style={inputStyle} />
          <p style={{ fontSize: 10, color: '#71717a', margin: '4px 0 0' }}>
            Submits a GET request with the query as ?q=... to this URL.
          </p>
        </div>
      )}
      {nodeProps.showButton !== undefined && (
        <div style={sectionGap}>
          <label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
            <input type="checkbox" checked={nodeProps.showButton !== false} onChange={(e) => setProp('showButton', e.target.checked)} />
            Show Search Button
          </label>
        </div>
      )}

      {nodeProps.method !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Method</label>
          <div style={{ display: 'flex', gap: 4 }}>
            {['GET', 'POST'].map((m) => (
              <button key={m} onClick={() => setProp('method', m)} style={btnActiveStyle(nodeProps.method === m)}>{m}</button>
            ))}
          </div>
        </div>
      )}

      {/* Input field props */}
      {nodeProps.label !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Label</label>
          <input type="text" value={nodeProps.label || ''} onChange={(e) => setProp('label', e.target.value)} style={inputStyle} />
        </div>
      )}
      {nodeProps.placeholder !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Placeholder</label>
          <input type="text" value={nodeProps.placeholder || ''} onChange={(e) => setProp('placeholder', e.target.value)} style={inputStyle} />
        </div>
      )}
      {nodeProps.name !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Field Name</label>
          <input type="text" value={nodeProps.name || ''} onChange={(e) => setProp('name', e.target.value)} style={inputStyle} />
        </div>
      )}
      {nodeProps.type !== undefined && typeof nodeProps.type === 'string' && (
        <div style={sectionGap}>
          <label style={labelStyle}>Input Type</label>
          <select value={nodeProps.type} onChange={(e) => setProp('type', e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }}>
            {['text', 'email', 'password', 'number', 'tel', 'url'].map((t) => (
              <option key={t} value={t}>{t}</option>
            ))}
          </select>
        </div>
      )}
      {nodeProps.required !== undefined && (
        <div style={sectionGap}>
          <label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
            <input type="checkbox" checked={nodeProps.required || false} onChange={(e) => setProp('required', e.target.checked)} />
            Required
          </label>
        </div>
      )}

      {/* Button text */}
      {nodeProps.text !== undefined && nodeProps.label === undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Button Text</label>
          <input type="text" value={nodeProps.text || ''} onChange={(e) => setProp('text', e.target.value)} style={inputStyle} />
        </div>
      )}

      {/* Subscribe form props */}
      {nodeProps.buttonText !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Button Text</label>
          <input type="text" value={nodeProps.buttonText || ''} onChange={(e) => setProp('buttonText', e.target.value)} style={inputStyle} />
        </div>
      )}
      {nodeProps.placeholderText !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Placeholder Text</label>
          <input type="text" value={nodeProps.placeholderText || ''} onChange={(e) => setProp('placeholderText', e.target.value)} style={inputStyle} />
        </div>
      )}
      {nodeProps.successMessage !== undefined && (
        <div style={sectionGap}>
          <label style={labelStyle}>Success Message</label>
          <input type="text" value={nodeProps.successMessage || ''} onChange={(e) => setProp('successMessage', e.target.value)} style={inputStyle} />
        </div>
      )}

      {/* Style */}
      <CollapsibleSection title="Style" defaultOpen={false}>
        <div className="guided-section">
          <SectionLabel>Background</SectionLabel>
          <ColorSwatchGrid colors={BG_COLORS} activeValue={style.backgroundColor} onSelect={(v: string) => setPropStyle('backgroundColor', v)} />
        </div>
        <div className="guided-section">
          <SectionLabel>Padding</SectionLabel>
          <PresetButtonGrid presets={SPACING_PRESETS} activeValue={style.padding as string} onSelect={(v) => setPropStyle('padding', v)} />
        </div>
        <div className="guided-section">
          <SectionLabel>Border Radius</SectionLabel>
          <PresetButtonGrid presets={RADIUS_PRESETS} activeValue={style.borderRadius as string} onSelect={(v) => setPropStyle('borderRadius', v)} />
        </div>
      </CollapsibleSection>

      {/* Box model: margin/padding (per-side), border, shadow, opacity --
          common enh-batch rollout, applies to the whole form family since
          they all spread `style` onto their root element. */}
      <CollapsibleSection title="Spacing, Border & Effects" defaultOpen={false}>
        <SpacingControl
          label="Margin"
          value={{ top: style.marginTop, right: style.marginRight, bottom: style.marginBottom, left: style.marginLeft }}
          onChange={(side, v) => setPropStyle(`margin${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)}
        />
        <SpacingControl
          label="Padding (per side)"
          value={{ top: style.paddingTop, right: style.paddingRight, bottom: style.paddingBottom, left: style.paddingLeft }}
          onChange={(side, v) => setPropStyle(`padding${SPACING_SIDE_KEYS.find((s) => s.side === side)!.suffix}`, v)}
        />
        <BorderControl
          value={parseBorderValue(style.border)}
          onChange={(v) => setPropStyle('border', buildBorderShorthand(v))}
        />
        <div className="guided-section">
          <SectionLabel>Box Shadow</SectionLabel>
          <PresetButtonGrid presets={SHADOW_PRESETS} activeValue={style.boxShadow as string} onSelect={(v) => setPropStyle('boxShadow', v)} />
        </div>
        <div className="guided-section">
          <SectionLabel>Opacity</SectionLabel>
          <input
            type="range"
            min={0}
            max={100}
            value={style.opacity !== undefined && style.opacity !== '' ? Math.round(Number(style.opacity) * 100) : 100}
            onChange={(e) => setPropStyle('opacity', String(Number(e.target.value) / 100))}
            style={{ width: '100%' }}
          />
        </div>
      </CollapsibleSection>

      {/* Animation & Visibility -- gated on the blank/false defaults added to
          each owned component's craft.props (ContactForm, FormContainer,
          InputField, TextareaField, FormButton, SubscribeForm, SearchBar).
          No toHtml change needed: the export's buildDataAttrs() already
          emits data-animation/data-hide-* from these exact prop names for
          every node. */}
      {nodeProps.animation !== undefined && (
        <CollapsibleSection title="Animation & Visibility" defaultOpen={false}>
          <AnimationControl
            value={{ animation: nodeProps.animation, animationDelay: nodeProps.animationDelay }}
            onChange={(v) => { setProp('animation', v.animation); setProp('animationDelay', v.animationDelay); }}
          />
          <VisibilityControl
            value={{ hideOnDesktop: nodeProps.hideOnDesktop, hideOnTablet: nodeProps.hideOnTablet, hideOnMobile: nodeProps.hideOnMobile }}
            onChange={(v) => {
              setProp('hideOnDesktop', v.hideOnDesktop);
              setProp('hideOnTablet', v.hideOnTablet);
              setProp('hideOnMobile', v.hideOnMobile);
            }}
          />
        </CollapsibleSection>
      )}
    </>
  );
};
