/**
 * SetupPage — standalone full-page wizard for initial instance configuration.
 *
 * UI-SPEC §Surface Architecture: Standalone full-page — no nav shell; owns entire viewport.
 * Renders the revised 4-step wizard (D-02/D-04/D-05):
 *   Step 1: Welcome (orient operator; pre-start note; no inputs)
 *   Step 2: Instance Configuration (collect app_url, oidc_issuer, oidc_client_id, vapid_public_key)
 *   Step 3: Calendar Credential (Fastmail email + app password; CalDAV validation)
 *   Terminal: "Setup complete" (Surface 7) — replaces wizard card after step 3 completes
 *
 * Step 2 validates DB connectivity (POST /api/setup/validate/db), OIDC discovery
 * (POST /api/setup/validate/oidc), and VAPID key pair (POST /api/setup/validate/vapid)
 * AFTER writing config (POST /api/setup/config). All three must pass to proceed.
 * Step 3 validates CalDAV PROPFIND (POST /api/setup/credential), then calls
 * POST /api/setup/complete to flip setup_complete.
 *
 * Security:
 *   T-12-13: wizard never displays/handles VAPID_PRIVATE_KEY or SESSION_SECRET
 *   T-12-14: all copy is plain-text JSX children — no HTML injection (T-14 contract)
 *   T-12-15: app password is type="password"; never stored client-side
 */

import { useState, useRef, useEffect } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { ShieldCheck, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
import {
  fetchSetupStatus,
  postSetupConfig,
  validateSetupDb,
  validateSetupOidc,
  validateSetupVapid,
  postSetupCredential,
  postSetupComplete,
  SetupAlreadyLockedError,
  type SetupConfigPayload,
  type SetupCredentialPayload,
} from '../api/client.js';

// ── Types ─────────────────────────────────────────────────────────────────────

type WizardStep = 1 | 2 | 3;
type TerminalState = 'complete' | 'locked' | null;

type ValidationRowState = 'idle' | 'pending' | 'success' | 'failure';

interface ValidationRowStatus {
  db: ValidationRowState;
  oidc: ValidationRowState;
  vapid: ValidationRowState;
  caldav: ValidationRowState;
}

export interface SetupPageProps {
  /** Pass true to immediately render the "Already Locked" screen (Surface 8). Used in tests. */
  alreadyLocked?: boolean;
}

// ── Styles ────────────────────────────────────────────────────────────────────

const pageStyle: React.CSSProperties = {
  minHeight: '100dvh',
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'center',
  justifyContent: 'flex-start',
  background: 'var(--color-surface, #ffffff)',
  fontFamily: 'var(--font-family-base)',
  color: 'var(--color-text-primary, #111318)',
};

const contentColStyle: React.CSSProperties = {
  maxWidth: '540px',
  width: '100%',
  margin: '0 auto',
  padding: 'var(--space-12, 48px) var(--space-6, 24px)',
};

const cardStyle: React.CSSProperties = {
  background: 'var(--color-surface, #ffffff)',
  border: '1px solid var(--color-border, #e2e4e9)',
  borderRadius: '8px',
  padding: 'var(--space-6, 24px)',
  boxShadow: '0 1px 4px rgba(0,0,0,0.06)',
};

const primaryBtnStyle = (disabled: boolean): React.CSSProperties => ({
  background: disabled ? 'var(--color-border, #e2e4e9)' : 'var(--color-member-0, #4a90d9)',
  color: '#ffffff',
  border: 'none',
  cursor: disabled ? 'default' : 'pointer',
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 600,
  minHeight: '44px',
  minWidth: '44px',
  padding: '0 var(--space-6, 24px)',
  borderRadius: 'var(--space-1, 4px)',
  fontFamily: 'var(--font-family-base)',
  transition: 'background 0.15s ease',
});

const ghostBtnStyle: React.CSSProperties = {
  background: 'none',
  border: 'none',
  cursor: 'pointer',
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 600,
  color: 'var(--color-text-secondary, #6b7280)',
  minHeight: '44px',
  minWidth: '44px',
  padding: '0 var(--space-4, 16px)',
  fontFamily: 'var(--font-family-base)',
  borderRadius: 'var(--space-1, 4px)',
};

const inputStyle = (hasError: boolean): React.CSSProperties => ({
  width: '100%',
  boxSizing: 'border-box',
  padding: 'var(--space-3, 12px) var(--space-4, 16px)',
  border: `1px solid ${hasError ? 'var(--color-destructive, #dc2626)' : 'var(--color-border, #e2e4e9)'}`,
  borderRadius: 'var(--space-1, 4px)',
  fontSize: 'var(--text-body-size, 15px)',
  color: 'var(--color-text-primary, #111318)',
  background: 'var(--color-surface, #ffffff)',
  fontFamily: 'var(--font-family-base)',
  outline: 'none',
});

const labelStyle: React.CSSProperties = {
  display: 'block',
  fontSize: 'var(--text-label-size, 13px)',
  fontWeight: 600,
  color: 'var(--color-text-primary, #111318)',
  marginBottom: 'var(--space-1, 4px)',
};

const helperStyle: React.CSSProperties = {
  fontSize: 'var(--text-label-size, 13px)',
  color: 'var(--color-text-secondary, #6b7280)',
  marginTop: 'var(--space-1, 4px)',
};

// ── Step Indicator (Surface 2) ───────────────────────────────────────────────

const STEP_LABELS = ['Welcome', 'Instance', 'Calendar', 'Complete'];

interface StepIndicatorProps {
  currentStep: WizardStep;
}

function StepIndicator({ currentStep }: StepIndicatorProps) {
  return (
    <ol
      role="list"
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 0,
        listStyle: 'none',
        padding: 0,
        margin: '0 0 var(--space-6, 24px) 0',
        minHeight: '44px',
      }}
      aria-label="Setup progress"
    >
      {STEP_LABELS.map((label, idx) => {
        const stepNum = idx + 1;
        const isCompleted = stepNum < currentStep;
        const isActive = stepNum === currentStep;

        const circleStyle: React.CSSProperties = {
          width: '28px',
          height: '28px',
          borderRadius: '50%',
          flexShrink: 0,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          background: isCompleted || isActive ? 'var(--color-member-0, #4a90d9)' : 'transparent',
          border: isCompleted || isActive ? 'none' : '2px solid var(--color-border, #e2e4e9)',
          color: isCompleted || isActive ? '#ffffff' : 'var(--color-text-muted, #9ca3af)',
          fontSize: 'var(--text-label-size, 13px)',
          fontWeight: 600,
        };

        return (
          <li
            key={label}
            role="listitem"
            aria-current={isActive ? 'step' : undefined}
            style={{
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              flex: 1,
              position: 'relative',
            }}
          >
            {/* Connector line before this step */}
            {idx > 0 && (
              <div
                aria-hidden="true"
                style={{
                  position: 'absolute',
                  top: '14px',
                  right: '50%',
                  width: '100%',
                  height: '1px',
                  background: isCompleted
                    ? 'var(--color-member-0, #4a90d9)'
                    : 'var(--color-border, #e2e4e9)',
                  zIndex: 0,
                }}
              />
            )}

            <div style={{ ...circleStyle, zIndex: 1, position: 'relative' }}>
              {isCompleted ? <CheckCircle size={16} aria-hidden="true" /> : <span>{stepNum}</span>}
            </div>

            <span
              style={{
                fontSize: '11px',
                fontWeight: 400,
                color: isActive
                  ? 'var(--color-text-primary, #111318)'
                  : 'var(--color-text-muted, #9ca3af)',
                marginTop: '4px',
                textAlign: 'center',
                whiteSpace: 'nowrap',
              }}
            >
              {label}
            </span>
          </li>
        );
      })}
    </ol>
  );
}

// ── Validation Row (Surface 5) ────────────────────────────────────────────────

interface ValidationRowProps {
  state: ValidationRowState;
  pendingText: string;
  successText: string;
  failureText: string;
}

function ValidationRow({ state, pendingText, successText, failureText }: ValidationRowProps) {
  if (state === 'idle') return null;

  return (
    <div
      role="status"
      aria-live="polite"
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 'var(--space-2, 8px)',
        fontSize: 'var(--text-body-size, 15px)',
        marginTop: 'var(--space-3, 12px)',
        color:
          state === 'failure'
            ? 'var(--color-destructive, #dc2626)'
            : 'var(--color-text-secondary, #6b7280)',
      }}
    >
      {state === 'pending' && (
        <>
          <Loader2
            size={16}
            aria-hidden="true"
            style={{ animation: 'spin 1s linear infinite', flexShrink: 0 }}
          />
          {pendingText}
        </>
      )}
      {state === 'success' && (
        <>
          <CheckCircle size={16} aria-hidden="true" style={{ flexShrink: 0 }} />
          {successText}
        </>
      )}
      {state === 'failure' && (
        <>
          <AlertCircle
            size={16}
            aria-hidden="true"
            style={{ color: 'var(--color-destructive, #dc2626)', flexShrink: 0 }}
          />
          {failureText}
        </>
      )}
    </div>
  );
}

// ── Action Row (Surface 6) ────────────────────────────────────────────────────

interface ActionRowProps {
  onBack?: () => void;
  onContinue: () => void;
  continueLabel?: string;
  continueDisabled?: boolean;
  isPending?: boolean;
}

function ActionRow({
  onBack,
  onContinue,
  continueLabel = 'Continue',
  continueDisabled = false,
  isPending = false,
}: ActionRowProps) {
  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'flex-end',
        gap: 'var(--space-3, 12px)',
        marginTop: 'var(--space-6, 24px)',
      }}
    >
      {onBack && (
        <button type="button" onClick={onBack} style={ghostBtnStyle}>
          Back
        </button>
      )}
      <button
        type="button"
        onClick={onContinue}
        disabled={continueDisabled || isPending}
        style={primaryBtnStyle(continueDisabled || isPending)}
      >
        {isPending ? 'Saving…' : continueLabel}
      </button>
    </div>
  );
}

// ── Step 1: Welcome ───────────────────────────────────────────────────────────

interface Step1Props {
  onContinue: () => void;
  stepHeadingRef: React.RefObject<HTMLHeadingElement | null>;
}

function Step1Welcome({ onContinue, stepHeadingRef }: Step1Props) {
  return (
    <div style={cardStyle}>
      <h2
        ref={stepHeadingRef}
        tabIndex={-1}
        style={{
          margin: '0 0 var(--space-2, 8px) 0',
          fontSize: 'var(--text-heading-size, 18px)',
          fontWeight: 600,
          lineHeight: 'var(--text-heading-line-height, 1.25)',
          color: 'var(--color-text-primary, #111318)',
          outline: 'none',
        }}
      >
        Welcome to FamilySync Setup
      </h2>

      <p
        style={{
          margin: '0 0 var(--space-6, 24px) 0',
          fontSize: 'var(--text-body-size, 15px)',
          fontWeight: 400,
          color: 'var(--color-text-secondary, #6b7280)',
          lineHeight: 1.5,
        }}
      >
        This wizard will guide you through configuring your self-hosted instance. Before continuing,
        run <code>npm run generate-secrets</code> from the repo to generate your instance secrets
        and add them to your Docker environment. You&apos;ll also need: your OIDC client credentials
        (Authelia) and a Fastmail account with an app password. This takes about 5 minutes.
      </p>

      {/* Before you start note block */}
      <div
        style={{
          background: 'var(--color-surface-dim, #f7f7f8)',
          borderRadius: '4px',
          padding: 'var(--space-3, 12px) var(--space-4, 16px)',
          marginBottom: 'var(--space-4, 16px)',
        }}
      >
        <div
          style={{
            fontSize: 'var(--text-label-size, 13px)',
            fontWeight: 600,
            color: 'var(--color-text-primary, #111318)',
            marginBottom: '4px',
          }}
        >
          Before you start
        </div>
        <div
          style={{
            fontSize: 'var(--text-body-size, 15px)',
            fontWeight: 400,
            color: 'var(--color-text-secondary, #6b7280)',
            lineHeight: 1.5,
          }}
        >
          Run <code>npm run generate-secrets</code> and add the output to your Docker environment
          block. These secrets cannot be recovered if lost.
        </div>
      </div>

      <ActionRow onContinue={onContinue} continueLabel="Continue" />
    </div>
  );
}

// ── Step 2: Instance Configuration ───────────────────────────────────────────

interface InstanceFields {
  appUrl: string;
  oidcIssuer: string;
  oidcClientId: string;
  vapidPublicKey: string;
}

interface Step2Props {
  onBack: () => void;
  onSuccess: () => void;
  stepHeadingRef: React.RefObject<HTMLHeadingElement | null>;
  /** Lifted to SetupPage so values survive step unmount (gap 4: Back preserves entries). */
  fields: InstanceFields;
  setFields: React.Dispatch<React.SetStateAction<InstanceFields>>;
}

function Step2Config({ onBack, onSuccess, stepHeadingRef, fields, setFields }: Step2Props) {
  const { appUrl, oidcIssuer, oidcClientId, vapidPublicKey } = fields;
  const setAppUrl = (v: string) => setFields((f) => ({ ...f, appUrl: v }));
  const setOidcIssuer = (v: string) => setFields((f) => ({ ...f, oidcIssuer: v }));
  const setOidcClientId = (v: string) => setFields((f) => ({ ...f, oidcClientId: v }));
  const setVapidPublicKey = (v: string) => setFields((f) => ({ ...f, vapidPublicKey: v }));
  const [fieldError, setFieldError] = useState<string | null>(null);

  // Gap 3 (frontend): fetch the env-derived, non-secret DB name so the
  // "database connection verified" row below has an on-screen referent.
  // Only dbName is surfaced — DB_HOST/DB_USER/DB_PASSWORD are never fetched (T-12-3DB).
  const { data: setupStatus } = useQuery({
    queryKey: ['setupStatus'],
    queryFn: fetchSetupStatus,
    staleTime: 0,
    retry: false,
  });
  const dbName = setupStatus?.dbName ?? '';

  const [validationRows, setValidationRows] = useState<
    Pick<ValidationRowStatus, 'db' | 'oidc' | 'vapid'>
  >({
    db: 'idle',
    oidc: 'idle',
    vapid: 'idle',
  });

  // Track overall state: null = not yet run, 'running', 'done' (both pass), 'failed'
  const [configSaved, setConfigSaved] = useState(false);
  const [bothPassed, setBothPassed] = useState(false);

  const configMutation = useMutation({
    mutationFn: (payload: SetupConfigPayload) => postSetupConfig(payload),
    onSuccess: async () => {
      setConfigSaved(true);
      setFieldError(null);
      // Sequential validation: DB → OIDC → VAPID
      setValidationRows({ db: 'pending', oidc: 'idle', vapid: 'idle' });
      try {
        await validateSetupDb();
        setValidationRows({ db: 'success', oidc: 'pending', vapid: 'idle' });
        try {
          await validateSetupOidc();
          setValidationRows({ db: 'success', oidc: 'success', vapid: 'pending' });
          try {
            await validateSetupVapid();
            setValidationRows({ db: 'success', oidc: 'success', vapid: 'success' });
            setBothPassed(true);
          } catch (vapidErr) {
            setValidationRows((prev) => ({ ...prev, vapid: 'failure' }));
            setFieldError(
              vapidErr instanceof Error
                ? vapidErr.message
                : 'VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.',
            );
          }
        } catch (oidcErr) {
          setValidationRows((prev) => ({ ...prev, oidc: 'failure' }));
          setFieldError(
            oidcErr instanceof Error
              ? oidcErr.message
              : 'OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server.',
          );
        }
      } catch (dbErr) {
        setValidationRows({ db: 'failure', oidc: 'idle', vapid: 'idle' });
        setFieldError(
          dbErr instanceof Error
            ? dbErr.message
            : 'Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your Docker environment and try again.',
        );
      }
    },
    onError: (err) => {
      setFieldError(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
    },
  });

  const isSaveValidating = configMutation.isPending;
  const isValidating =
    validationRows.db === 'pending' ||
    validationRows.oidc === 'pending' ||
    validationRows.vapid === 'pending';
  const anyPending = isSaveValidating || isValidating;

  function handleSaveAndValidate() {
    setFieldError(null);
    setBothPassed(false);
    setConfigSaved(false);
    setValidationRows({ db: 'idle', oidc: 'idle', vapid: 'idle' });

    if (!appUrl.trim() || !oidcIssuer.trim() || !oidcClientId.trim() || !vapidPublicKey.trim()) {
      setFieldError('All fields are required.');
      return;
    }

    configMutation.mutate({
      appExternalUrl: appUrl.trim(),
      oidcIssuer: oidcIssuer.trim(),
      oidcClientId: oidcClientId.trim(),
      vapidPublicKey: vapidPublicKey.trim(),
    });
  }

  return (
    <div style={cardStyle}>
      <h2
        ref={stepHeadingRef}
        tabIndex={-1}
        style={{
          margin: '0 0 var(--space-2, 8px) 0',
          fontSize: 'var(--text-heading-size, 18px)',
          fontWeight: 600,
          lineHeight: 'var(--text-heading-line-height, 1.25)',
          color: 'var(--color-text-primary, #111318)',
          outline: 'none',
        }}
      >
        Instance Configuration
      </h2>

      <p
        style={{
          margin: '0 0 var(--space-6, 24px) 0',
          fontSize: 'var(--text-body-size, 15px)',
          fontWeight: 400,
          color: 'var(--color-text-secondary, #6b7280)',
          lineHeight: 1.5,
        }}
      >
        Enter your instance&apos;s connection details.
      </p>

      {/* App URL */}
      <div style={{ marginBottom: 'var(--space-4, 16px)' }}>
        <label htmlFor="setup-app-url" style={labelStyle}>
          App URL
        </label>
        <input
          id="setup-app-url"
          type="text"
          value={appUrl}
          onChange={(e) => setAppUrl(e.target.value)}
          placeholder="https://familysync.example.com"
          style={inputStyle(false)}
          autoComplete="url"
        />
        <div style={helperStyle}>The public URL where FamilySync is reachable.</div>
      </div>

      {/* Database (read-only, env-derived) — gives the DB validation row below a referent */}
      <div style={{ marginBottom: 'var(--space-4, 16px)' }}>
        <label htmlFor="setup-db-name" style={labelStyle}>
          Database
        </label>
        <input
          id="setup-db-name"
          type="text"
          value={dbName || '—'}
          readOnly
          disabled
          aria-readonly="true"
          tabIndex={-1}
          style={{
            ...inputStyle(false),
            background: 'var(--color-surface-dim, #f7f7f8)',
            color: 'var(--color-text-secondary, #6b7280)',
            cursor: 'default',
          }}
        />
        <div style={helperStyle}>
          Configured via the server&apos;s Docker environment (<code>DB_HOST</code>,{' '}
          <code>DB_PORT</code>, <code>DB_USER</code>, <code>DB_PASSWORD</code>) — not entered here.
        </div>
      </div>

      {/* OIDC Issuer */}
      <div style={{ marginBottom: 'var(--space-4, 16px)' }}>
        <label htmlFor="setup-oidc-issuer" style={labelStyle}>
          OIDC issuer URL
        </label>
        <input
          id="setup-oidc-issuer"
          type="text"
          value={oidcIssuer}
          onChange={(e) => setOidcIssuer(e.target.value)}
          placeholder="https://auth.example.com"
          style={inputStyle(false)}
        />
        <div style={helperStyle}>
          Your Authelia instance URL. FamilySync will fetch{' '}
          <code>/.well-known/openid-configuration</code> from this URL.
        </div>
      </div>

      {/* OIDC Client ID */}
      <div style={{ marginBottom: 'var(--space-4, 16px)' }}>
        <label htmlFor="setup-oidc-client-id" style={labelStyle}>
          OIDC client ID
        </label>
        <input
          id="setup-oidc-client-id"
          type="text"
          value={oidcClientId}
          onChange={(e) => setOidcClientId(e.target.value)}
          placeholder="familysync"
          style={inputStyle(false)}
        />
        <div style={helperStyle}>The client ID registered in Authelia for this application.</div>
      </div>

      {/* VAPID Public Key */}
      <div style={{ marginBottom: 'var(--space-4, 16px)' }}>
        <label htmlFor="setup-vapid-public-key" style={labelStyle}>
          VAPID public key
        </label>
        <input
          id="setup-vapid-public-key"
          type="text"
          value={vapidPublicKey}
          onChange={(e) => setVapidPublicKey(e.target.value)}
          placeholder="BH…"
          style={inputStyle(false)}
        />
        <div style={helperStyle}>
          Paste the <code>VAPID_PUBLIC_KEY</code> value from <code>npm run generate-secrets</code>.
        </div>
      </div>

      {/* Save & Validate button — full width above validation rows */}
      <button
        type="button"
        onClick={handleSaveAndValidate}
        disabled={anyPending}
        style={{
          ...primaryBtnStyle(anyPending),
          width: '100%',
          marginBottom: 'var(--space-3, 12px)',
        }}
      >
        {anyPending ? 'Validating…' : 'Save & Validate'}
      </button>

      {/* Validation state rows */}
      <ValidationRow
        state={validationRows.db}
        pendingText="Testing database connection…"
        successText="Database connection verified."
        failureText={
          fieldError ?? 'Cannot reach the database. Check your Docker environment and try again.'
        }
      />
      <ValidationRow
        state={validationRows.oidc}
        pendingText="Checking OIDC discovery…"
        successText="OIDC discovery resolved."
        failureText={
          fieldError ??
          'OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server.'
        }
      />
      <ValidationRow
        state={validationRows.vapid}
        pendingText="Validating VAPID key pair…"
        successText="VAPID keys verified."
        failureText={
          fieldError ??
          'VAPID validation failed. Check that your VAPID keys were generated with `npm run generate-secrets`.'
        }
      />

      {/* General field error (before validation rows show) */}
      {fieldError &&
        validationRows.db === 'idle' &&
        validationRows.oidc === 'idle' &&
        validationRows.vapid === 'idle' && (
          <div
            role="status"
            aria-live="polite"
            style={{
              color: 'var(--color-destructive, #dc2626)',
              fontSize: 'var(--text-body-size, 15px)',
              marginTop: 'var(--space-3, 12px)',
            }}
          >
            {fieldError}
          </div>
        )}

      {/* Continue + Back row — appears only when both validations pass */}
      {bothPassed && configSaved && (
        <ActionRow onBack={onBack} onContinue={onSuccess} continueLabel="Continue" />
      )}

      {/* Always show Back if not pending and not already showing both-passed row */}
      {!bothPassed && !anyPending && (
        <div
          style={{
            display: 'flex',
            justifyContent: 'flex-start',
            marginTop: 'var(--space-3, 12px)',
          }}
        >
          <button type="button" onClick={onBack} style={ghostBtnStyle}>
            Back
          </button>
        </div>
      )}
    </div>
  );
}

// ── Step 3: Calendar Credential ───────────────────────────────────────────────

interface Step3Props {
  onBack: () => void;
  onSuccess: () => void;
  onLocked: () => void;
  stepHeadingRef: React.RefObject<HTMLHeadingElement | null>;
}

function Step3Credential({ onBack, onSuccess, onLocked, stepHeadingRef }: Step3Props) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [validationState, setValidationState] = useState<ValidationRowState>('idle');
  const [errorText, setErrorText] = useState<string | null>(null);
  const [credentialVerified, setCredentialVerified] = useState(false);

  const completeMutation = useMutation({
    mutationFn: async (payload: SetupCredentialPayload) => {
      setValidationState('pending');
      setErrorText(null);
      await postSetupCredential(payload);
      setValidationState('success');
      setCredentialVerified(true);
    },
    onError: (err) => {
      if (err instanceof SetupAlreadyLockedError) {
        onLocked();
        return;
      }
      setValidationState('failure');
      setErrorText(
        err instanceof Error
          ? err.message
          : "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again.",
      );
    },
  });

  const finalMutation = useMutation({
    mutationFn: postSetupComplete,
    onSuccess: () => {
      onSuccess();
    },
    onError: (err) => {
      if (err instanceof SetupAlreadyLockedError) {
        onLocked();
        return;
      }
      setErrorText(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
    },
  });

  const isPending = completeMutation.isPending || finalMutation.isPending;
  const saveDisabled = isPending || email.trim().length === 0 || password.trim().length === 0;

  function handleValidate() {
    if (saveDisabled) return;
    setValidationState('idle');
    setCredentialVerified(false);
    completeMutation.mutate({ fastmailEmail: email.trim(), appPassword: password });
  }

  function handleComplete() {
    finalMutation.mutate();
  }

  return (
    <div style={cardStyle}>
      <h2
        ref={stepHeadingRef}
        tabIndex={-1}
        style={{
          margin: '0 0 var(--space-2, 8px) 0',
          fontSize: 'var(--text-heading-size, 18px)',
          fontWeight: 600,
          lineHeight: 'var(--text-heading-line-height, 1.25)',
          color: 'var(--color-text-primary, #111318)',
          outline: 'none',
        }}
      >
        Fastmail Credential
      </h2>

      <p
        style={{
          margin: '0 0 var(--space-6, 24px) 0',
          fontSize: 'var(--text-body-size, 15px)',
          fontWeight: 400,
          color: 'var(--color-text-secondary, #6b7280)',
          lineHeight: 1.5,
        }}
      >
        Add the Fastmail app password for the first household member. This credential is validated
        against Fastmail CalDAV before saving. The password is never stored in plain text.
      </p>

      {/* Fastmail email */}
      <div style={{ marginBottom: 'var(--space-4, 16px)' }}>
        <label htmlFor="setup-cred-email" style={labelStyle}>
          Fastmail email
        </label>
        <input
          id="setup-cred-email"
          type="email"
          autoComplete="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="user@fastmail.com"
          style={inputStyle(validationState === 'failure')}
        />
      </div>

      {/* App password */}
      <div style={{ marginBottom: 'var(--space-2, 8px)' }}>
        <label htmlFor="setup-cred-password" style={labelStyle}>
          App password
        </label>
        <input
          id="setup-cred-password"
          type="password"
          autoComplete="new-password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          aria-describedby="setup-cred-helper"
          style={inputStyle(validationState === 'failure')}
        />
      </div>

      {/* Helper text */}
      <div
        id="setup-cred-helper"
        style={{
          ...helperStyle,
          marginBottom: 'var(--space-6, 24px)',
        }}
      >
        Enter the Fastmail app password scoped to Calendars/CalDAV.{' '}
        <a
          href="https://app.fastmail.com/settings/security/devicetokens"
          target="_blank"
          rel="noopener noreferrer"
          style={{
            color: 'var(--color-member-0, #4a90d9)',
            textDecoration: 'underline',
          }}
        >
          Get an app password
        </a>
        {" — choose the 'Calendars & Contacts (CalDAV)' scope."}
      </div>

      {/* Validation state row */}
      <div role="status" aria-live="polite">
        {validationState !== 'idle' && (
          <ValidationRow
            state={validationState}
            pendingText="Validating against CalDAV…"
            successText="Credential verified."
            failureText={
              errorText ??
              "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again."
            }
          />
        )}
      </div>

      {/* Action row */}
      <div
        style={{
          display: 'flex',
          justifyContent: 'flex-end',
          gap: 'var(--space-3, 12px)',
          marginTop: 'var(--space-6, 24px)',
        }}
      >
        <button type="button" onClick={onBack} disabled={isPending} style={ghostBtnStyle}>
          Back
        </button>

        {!credentialVerified ? (
          <button
            type="button"
            onClick={handleValidate}
            disabled={saveDisabled}
            style={primaryBtnStyle(saveDisabled)}
          >
            {completeMutation.isPending ? 'Validating…' : 'Validate Credential'}
          </button>
        ) : (
          <button
            type="button"
            onClick={handleComplete}
            disabled={finalMutation.isPending}
            style={primaryBtnStyle(finalMutation.isPending)}
          >
            {finalMutation.isPending ? 'Completing…' : 'Complete Setup'}
          </button>
        )}
      </div>
    </div>
  );
}

// ── Surface 7: Terminal "Setup Complete" ──────────────────────────────────────

function TerminalComplete() {
  return (
    <div
      style={{
        textAlign: 'center',
        padding: 'var(--space-12, 48px) var(--space-6, 24px)',
      }}
    >
      <ShieldCheck
        size={48}
        aria-hidden="true"
        style={{ color: 'var(--color-member-0, #4a90d9)' }}
      />
      <h2
        style={{
          margin: 'var(--space-4, 16px) 0 0 0',
          fontSize: 'var(--text-display-size, 24px)',
          fontWeight: 600,
          lineHeight: 'var(--text-display-line-height, 1.2)',
          color: 'var(--color-text-primary, #111318)',
        }}
      >
        Setup complete
      </h2>
      <p
        style={{
          margin: 'var(--space-2, 8px) 0 0 0',
          fontSize: 'var(--text-body-size, 15px)',
          fontWeight: 400,
          color: 'var(--color-text-secondary, #6b7280)',
        }}
      >
        Your FamilySync instance is ready. Sign in to continue.
      </p>
      <div style={{ marginTop: 'var(--space-6, 24px)' }}>
        <a
          href="/"
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            justifyContent: 'center',
            background: 'var(--color-member-0, #4a90d9)',
            color: '#ffffff',
            textDecoration: 'none',
            fontSize: 'var(--text-label-size, 13px)',
            fontWeight: 600,
            minHeight: '44px',
            padding: '0 var(--space-6, 24px)',
            borderRadius: 'var(--space-1, 4px)',
            transition: 'background 0.15s ease',
          }}
        >
          Sign in
        </a>
      </div>
    </div>
  );
}

// ── Surface 8: Already Locked ─────────────────────────────────────────────────

function AlreadyLocked() {
  return (
    <div
      style={{
        textAlign: 'center',
        padding: 'var(--space-12, 48px) var(--space-6, 24px)',
      }}
    >
      <ShieldCheck
        size={48}
        aria-hidden="true"
        style={{ color: 'var(--color-text-muted, #9ca3af)' }}
      />
      <h2
        style={{
          margin: 'var(--space-4, 16px) 0 var(--space-2, 8px) 0',
          fontSize: 'var(--text-heading-size, 18px)',
          fontWeight: 600,
          color: 'var(--color-text-primary, #111318)',
        }}
      >
        Setup already complete
      </h2>
      <p
        style={{
          margin: '0',
          fontSize: 'var(--text-body-size, 15px)',
          fontWeight: 400,
          color: 'var(--color-text-secondary, #6b7280)',
        }}
      >
        This instance has already been configured.{' '}
        <a
          href="/"
          style={{
            color: 'var(--color-member-0, #4a90d9)',
            textDecoration: 'underline',
            fontWeight: 600,
          }}
        >
          Sign in
        </a>{' '}
        to continue.
      </p>
    </div>
  );
}

// ── SetupPage ─────────────────────────────────────────────────────────────────

export function SetupPage({ alreadyLocked = false }: SetupPageProps) {
  const [step, setStep] = useState<WizardStep>(1);
  const [terminal, setTerminal] = useState<TerminalState>(alreadyLocked ? 'locked' : null);
  const stepHeadingRef = useRef<HTMLHeadingElement>(null);

  // Gap 4: Instance-step field values are lifted here so they survive Step2 unmount.
  // The Fastmail app password (Step 3) is deliberately NOT lifted — it stays in
  // Step3Credential local state and is cleared on unmount (T-12-15 preserved).
  const [instanceFields, setInstanceFields] = useState<InstanceFields>({
    appUrl: '',
    oidcIssuer: '',
    oidcClientId: '',
    vapidPublicKey: '',
  });

  // Focus the step heading on step change for a11y (D-04 focus management)
  useEffect(() => {
    if (stepHeadingRef.current) {
      stepHeadingRef.current.focus();
    }
  }, [step]);

  // Locked state — render Surface 8 immediately
  if (terminal === 'locked') {
    return (
      <div style={pageStyle}>
        <div style={contentColStyle} role="main">
          <h1
            style={{
              margin: '0 0 var(--space-2, 8px) 0',
              fontSize: 'var(--text-display-size, 24px)',
              fontWeight: 600,
              lineHeight: 'var(--text-display-line-height, 1.2)',
              color: 'var(--color-text-primary, #111318)',
            }}
          >
            FamilySync Setup
          </h1>
          <AlreadyLocked />
        </div>
      </div>
    );
  }

  // Complete state — render Surface 7
  if (terminal === 'complete') {
    return (
      <div style={pageStyle}>
        <div style={contentColStyle} role="main">
          <h1
            style={{
              margin: '0 0 var(--space-2, 8px) 0',
              fontSize: 'var(--text-display-size, 24px)',
              fontWeight: 600,
              lineHeight: 'var(--text-display-line-height, 1.2)',
              color: 'var(--color-text-primary, #111318)',
            }}
          >
            FamilySync Setup
          </h1>
          <TerminalComplete />
        </div>
      </div>
    );
  }

  // Active wizard
  return (
    <div style={pageStyle}>
      <div style={contentColStyle} role="main">
        {/* Page title */}
        <h1
          style={{
            margin: '0 0 var(--space-2, 8px) 0',
            fontSize: 'var(--text-display-size, 24px)',
            fontWeight: 600,
            lineHeight: 'var(--text-display-line-height, 1.2)',
            color: 'var(--color-text-primary, #111318)',
          }}
        >
          FamilySync Setup
        </h1>
        <p
          style={{
            margin: '0 0 var(--space-8, 32px) 0',
            fontSize: 'var(--text-body-size, 15px)',
            fontWeight: 400,
            color: 'var(--color-text-secondary, #6b7280)',
            lineHeight: 1.5,
          }}
        >
          Let&apos;s get your instance ready.
        </p>

        {/* Step indicator (Surface 2) */}
        <StepIndicator currentStep={step} />

        {/* Step card (Surface 3) */}
        {step === 1 && (
          <Step1Welcome onContinue={() => setStep(2)} stepHeadingRef={stepHeadingRef} />
        )}
        {step === 2 && (
          <Step2Config
            onBack={() => setStep(1)}
            onSuccess={() => setStep(3)}
            stepHeadingRef={stepHeadingRef}
            fields={instanceFields}
            setFields={setInstanceFields}
          />
        )}
        {step === 3 && (
          <Step3Credential
            onBack={() => setStep(2)}
            onSuccess={() => setTerminal('complete')}
            onLocked={() => setTerminal('locked')}
            stepHeadingRef={stepHeadingRef}
          />
        )}
      </div>
    </div>
  );
}
