// SSO providers — admin page at #/identity/identity_providers (under
// Identity → Connections). Plan #30 consolidated IAM under Identity
// and renamed the URL slug; old #/admin/sso and #/iam/sso URLs alias
// through parseHashRoute.
//
// CRUD over /v1/identity/sso/providers (backend service name unchanged).
// PR5 (SSO via OIDC) shipped the REST surface; PR6 (SAML) added the
// `config` jsonb. This page focuses on OIDC create/edit since SAML
// providers need an entity_id / acs_url shape that doesn't fit the
// same flat form. Existing SAML providers render in the list with a
// chip + read-only edit modal so admins can still see the wiring
// without falling off the page.
//
// Per-provider client_secret is NOT entered here — secrets live in
// helm/secrets/templates/sso-client-secrets.yaml as
// SSO_CLIENT_SECRET_<UUID> entries. After a successful create the page
// surfaces a copy-paste snippet so the operator can add the secret on
// the side. Until they do, the provider exists but `/v1/sso/start`
// will fail to exchange the code.

function _ssoBtn(variant) {
  const base = {
    padding:'6px 12px', fontSize:11, letterSpacing:'0.08em',
    border:'1px solid var(--border)', background:'var(--bg2)',
    color:'var(--text)', cursor:'pointer',
    fontFamily:'IBM Plex Mono,monospace', textTransform:'uppercase',
  };
  if (variant === 'primary') {
    return { ...base, background:'var(--sc-green)', borderColor:'var(--sc-green)', color:'#ffffff' };
  }
  if (variant === 'danger') {
    return {
      padding:'3px 8px', fontSize:9, letterSpacing:'0.06em',
      fontFamily:'IBM Plex Mono,monospace', cursor:'pointer',
      border:'1px solid #d13644', background:'#d13644',
      color:'#ffffff', borderRadius:2, textTransform:'uppercase',
    };
  }
  if (variant === 'row') {
    // Edit button — neutral chrome, matches the size + edge profile of
    // the row-level Revoke button so per-row actions read consistently
    // across admin tables.
    return {
      padding:'3px 8px', fontSize:9, letterSpacing:'0.06em',
      fontFamily:'IBM Plex Mono,monospace', cursor:'pointer',
      border:'1px solid var(--border)', background:'var(--bg2)',
      color:'var(--text)', borderRadius:2, textTransform:'uppercase',
    };
  }
  return base;
}

// Reusable form-control style — kept inline to match the api-tokens
// modal so the two admin pages render visually identical inputs.
const _SSO_INPUT_STYLE = {
  padding:'6px 10px', border:'1px solid var(--border)',
  background:'var(--bg)', color:'var(--text)',
  fontFamily:'IBM Plex Mono,monospace',
};
const _SSO_LABEL_STYLE = {
  display:'flex', flexDirection:'column', gap:4,
  fontSize:11, letterSpacing:'0.08em',
};

// CreateOrEditProviderModal handles both new and existing rows. On
// create (initial == null) the form starts blank and submits POST;
// on edit it pre-fills from `initial` and submits PATCH. SAML rows
// are read-only in this modal — the editor for SAML `config` lands
// in a follow-up plan.
function CreateOrEditProviderModal({ initial, groups, onClose, onSaved }) {
  const isEdit = !!initial;
  const isSAML = isEdit && initial.kind === 'saml';

  const [name, setName]               = useState(initial?.name        || '');
  const [issuer, setIssuer]           = useState(initial?.issuer      || '');
  const [clientID, setClientID]       = useState(initial?.client_id   || '');
  const [redirectURI, setRedirectURI] = useState(initial?.redirect_uri|| '');
  const [domain, setDomain]           = useState(initial?.domain_claim|| '');
  const [defaultGroup, setDefaultGroup] = useState(initial?.default_group_id || '');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  const submit = async (e) => {
    e.preventDefault();
    if (isSAML) {
      // Read-only path — should never fire because the submit button
      // is disabled, but belt-and-braces.
      onClose();
      return;
    }
    const body = {
      kind: 'oidc',
      name: name.trim(),
      issuer: issuer.trim(),
      client_id: clientID.trim(),
      redirect_uri: redirectURI.trim(),
      domain_claim: domain.trim() || null,
      default_group_id: defaultGroup || null,
    };
    setBusy(true);
    setError(null);
    try {
      const saved = isEdit
        ? await apiFetch('identity', `/v1/identity/sso/providers/${initial.id}`, { method:'PATCH', body })
        : await apiFetch('identity', `/v1/identity/sso/providers`,                { method:'POST',  body });
      onSaved(saved, !isEdit);
    } catch (err) {
      setError(String(err.message || err));
    } finally {
      setBusy(false);
    }
  };

  const title = isEdit
    ? (isSAML ? 'SAML provider (read-only)' : 'Edit SSO provider')
    : 'Create SSO provider';

  return (
    <Modal title={title} onClose={onClose}>
      <form onSubmit={submit} style={{ display:'flex', flexDirection:'column', gap:14 }}>
        {isSAML && (
          <div style={{
            padding:'10px 12px', fontSize:11, lineHeight:1.5,
            background:'rgba(61,139,220,0.08)', border:'1px solid #3d8bdc',
            color:'var(--text)',
          }}>
            SAML providers use the <code>config</code> jsonb (entity_id /
            acs_url / idp_sso_url / idp_metadata_url or idp_cert_pem).
            This modal is read-only for kind=saml — edit via the iam
            REST surface for now; SAML UI is a follow-up plan.
          </div>
        )}
        <label style={_SSO_LABEL_STYLE}>
          NAME
          <input type="text" value={name} onChange={e => setName(e.target.value)}
            required maxLength={120} placeholder="Okta — corp"
            disabled={isSAML} style={_SSO_INPUT_STYLE} />
        </label>
        {!isSAML && (
          <>
            <label style={_SSO_LABEL_STYLE}>
              ISSUER
              <input type="url" value={issuer} onChange={e => setIssuer(e.target.value)}
                required placeholder="https://acme.okta.com"
                style={_SSO_INPUT_STYLE} />
            </label>
            <label style={_SSO_LABEL_STYLE}>
              CLIENT ID
              <input type="text" value={clientID} onChange={e => setClientID(e.target.value)}
                required placeholder="0oa1b2c3d4e5f6g7h8" style={_SSO_INPUT_STYLE} />
            </label>
            <label style={_SSO_LABEL_STYLE}>
              REDIRECT URI
              <input type="url" value={redirectURI} onChange={e => setRedirectURI(e.target.value)}
                required placeholder="https://app.example.com/v1/sso/oidc/<id>/callback"
                style={_SSO_INPUT_STYLE} />
              <small style={{ color:'var(--text3)' }}>
                Register this exact URL at the IdP. The path segment after <code>oidc/</code> is the provider id you'll see in the list after saving.
              </small>
            </label>
          </>
        )}
        {isSAML && (
          <label style={_SSO_LABEL_STYLE}>
            CONFIG (read-only)
            <textarea readOnly rows={6} value={_prettyJSON(initial.config)}
              style={{ ..._SSO_INPUT_STYLE, fontSize:11 }} />
          </label>
        )}
        <label style={_SSO_LABEL_STYLE}>
          DOMAIN CLAIM (optional)
          <input type="text" value={domain} onChange={e => setDomain(e.target.value)}
            placeholder="acmecorp.com" disabled={isSAML} style={_SSO_INPUT_STYLE} />
          <small style={{ color:'var(--text3)' }}>
            Users on <code>*@&lt;domain&gt;</code> are forced through this provider — password login is refused for those addresses.
          </small>
        </label>
        <label style={_SSO_LABEL_STYLE}>
          DEFAULT GROUP (optional)
          <select value={defaultGroup} onChange={e => setDefaultGroup(e.target.value)}
            disabled={isSAML} style={_SSO_INPUT_STYLE}>
            <option value="">— no auto-enrolment —</option>
            {(groups || []).map(g => (
              <option key={g.id} value={g.id}>{g.name}</option>
            ))}
          </select>
          <small style={{ color:'var(--text3)' }}>
            Newly JIT-provisioned users land in this group. Leave blank to require manual enrolment.
          </small>
        </label>
        {error && <div style={{ color:'#d13644', fontSize:11 }}>{error}</div>}
        <div style={{ display:'flex', gap:8, justifyContent:'flex-end' }}>
          <button type="button" onClick={onClose} style={_ssoBtn()}>{isSAML ? 'CLOSE' : 'CANCEL'}</button>
          {!isSAML && (
            <button type="submit" disabled={busy} style={_ssoBtn('primary')}>
              {busy ? (isEdit ? 'SAVING…' : 'CREATING…') : (isEdit ? 'SAVE' : 'CREATE')}
            </button>
          )}
        </div>
      </form>
    </Modal>
  );
}

// _prettyJSON tolerates both an already-parsed object and a JSON string
// (iam returns config as a JSON-encoded string on the wire for SAML
// rows; OIDC rows just send `{}`). Empty / unparseable → '{}'.
function _prettyJSON(v) {
  if (v == null) return '{}';
  if (typeof v === 'string') {
    try { return JSON.stringify(JSON.parse(v), null, 2); }
    catch (_) { return v; }
  }
  try { return JSON.stringify(v, null, 2); }
  catch (_) { return '{}'; }
}

// SecretSnippetCallout — shown once after a successful create. Surfaces
// the env-var key + a copy-paste kubectl helper so an operator can wire
// the client_secret without leaving the page. Dismissable; not
// re-shown after dismissal (the page never round-trips the secret
// anywhere, so there's nothing to recover).
function SecretSnippetCallout({ provider, onDismiss }) {
  const key = `SSO_CLIENT_SECRET_${String(provider.id).replace(/-/g, '_').toUpperCase()}`;
  const snippet =
    `kubectl -n yotta-drone create secret generic sso-client-secrets \\\n` +
    `  --from-literal=${key}=<your-client-secret> \\\n` +
    `  --dry-run=client -o yaml | kubectl apply -f -\n` +
    `kubectl -n yotta-drone rollout restart deploy/auth`;
  const [copied, setCopied] = useState(false);
  const copy = async () => {
    try { await navigator.clipboard.writeText(snippet); setCopied(true); }
    catch (_) { setCopied(false); }
  };
  return (
    <div style={{
      padding:'14px 16px', margin:'14px 14px 0', borderRadius:0,
      background:'rgba(124,189,60,0.10)', border:'1px solid #7cbd3c',
      color:'var(--text)',
    }}>
      <div style={{ fontSize:11, letterSpacing:'0.12em', textTransform:'uppercase',
                    color:'#7cbd3c', marginBottom:8, fontFamily:'IBM Plex Mono,monospace' }}>
        NEXT STEP — ADD THE CLIENT SECRET TO THE CLUSTER
      </div>
      <div style={{ fontSize:11, marginBottom:8 }}>
        The provider <strong>{provider.name}</strong> is saved. The
        OIDC client secret lives in the <code>sso-client-secrets</code> Secret
        (one entry per provider). Run the snippet below to add it, then
        the IdP login flow will work:
      </div>
      <pre style={{
        fontFamily:'IBM Plex Mono,monospace', fontSize:12,
        padding:'8px 10px', background:'var(--bg)',
        border:'1px solid var(--border)', whiteSpace:'pre-wrap',
        wordBreak:'break-all', userSelect:'all', margin:0,
      }}>{snippet}</pre>
      <div style={{ display:'flex', gap:8, marginTop:10 }}>
        <button onClick={copy} style={_ssoBtn('primary')}>{copied ? 'COPIED' : 'COPY'}</button>
        <button onClick={onDismiss} style={_ssoBtn()}>DISMISS</button>
      </div>
    </div>
  );
}

// CreateSSOProviderPage — page-mounted OIDC create form. Replaces the
// "+ CREATE" branch of the old combined modal; EDIT still uses the
// modal because SAML rows need a read-only view and the form is
// otherwise identical, and the user-requested change was for CREATE
// only.
function CreateSSOProviderPage({ onBack, navigate }) {
  const [name, setName]               = useState('');
  const [issuer, setIssuer]           = useState('');
  const [clientID, setClientID]       = useState('');
  const [redirectURI, setRedirectURI] = useState('');
  const [domain, setDomain]           = useState('');
  const [defaultGroup, setDefaultGroup] = useState('');
  const [groups, setGroups] = useState([]);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const [created, setCreated] = useState(null);

  useEffect(() => {
    let cancelled = false;
    apiFetch('identity', '/v1/identity/groups')
      .then(g => { if (!cancelled) setGroups(g || []); })
      .catch(() => { /* leave empty */ });
    return () => { cancelled = true; };
  }, []);

  const submit = async (e) => {
    e.preventDefault();
    const body = {
      kind: 'oidc',
      name: name.trim(),
      issuer: issuer.trim(),
      client_id: clientID.trim(),
      redirect_uri: redirectURI.trim(),
      domain_claim: domain.trim() || null,
      default_group_id: defaultGroup || null,
    };
    setBusy(true);
    setError(null);
    try {
      const saved = await apiFetch('identity', '/v1/identity/sso/providers', { method:'POST', body });
      setCreated(saved);
    } catch (err) {
      setError(String(err.message || err));
    } finally {
      setBusy(false);
    }
  };

  const crumbs = (
    <Breadcrumbs crumbs={[
      { label:'Identity' },
      { label:'Identity providers', onClick:onBack },
      { label: created ? 'Created' : 'New' },
    ]} />
  );

  if (created) {
    return (
      <>
        {crumbs}
        <div style={{ padding:'18px 20px 24px', maxWidth:760, display:'flex', flexDirection:'column', gap:14 }}>
          <div style={{ fontSize:11, fontWeight:700, letterSpacing:'0.14em', color:'var(--text)', textTransform:'uppercase' }}>
            SSO provider created
          </div>
          <SecretSnippetCallout provider={created} onDismiss={onBack} />
          <div>
            <button onClick={onBack} style={_ssoBtn('primary')}>DONE</button>
          </div>
        </div>
      </>
    );
  }

  return (
    <>
      {crumbs}
      <div style={{ padding:'18px 20px 24px', maxWidth:760 }}>
        <div style={{ fontSize:11, fontWeight:700, letterSpacing:'0.14em', color:'var(--text)', marginBottom:14, textTransform:'uppercase' }}>
          Create SSO provider
        </div>
        <form onSubmit={submit} style={{ display:'flex', flexDirection:'column', gap:14 }}>
          <label style={_SSO_LABEL_STYLE}>
            NAME
            <input type="text" value={name} onChange={e => setName(e.target.value)}
              required maxLength={120} placeholder="Okta — corp" style={_SSO_INPUT_STYLE} />
          </label>
          <label style={_SSO_LABEL_STYLE}>
            ISSUER
            <input type="url" value={issuer} onChange={e => setIssuer(e.target.value)}
              required placeholder="https://acme.okta.com" style={_SSO_INPUT_STYLE} />
          </label>
          <label style={_SSO_LABEL_STYLE}>
            CLIENT ID
            <input type="text" value={clientID} onChange={e => setClientID(e.target.value)}
              required placeholder="0oa1b2c3d4e5f6g7h8" style={_SSO_INPUT_STYLE} />
          </label>
          <label style={_SSO_LABEL_STYLE}>
            REDIRECT URI
            <input type="url" value={redirectURI} onChange={e => setRedirectURI(e.target.value)}
              required placeholder="https://app.example.com/v1/sso/oidc/<id>/callback"
              style={_SSO_INPUT_STYLE} />
            <small style={{ color:'var(--text3)' }}>
              Register this exact URL at the IdP. The path segment after <code>oidc/</code> is the provider id you'll see in the list after saving.
            </small>
          </label>
          <label style={_SSO_LABEL_STYLE}>
            DOMAIN CLAIM (optional)
            <input type="text" value={domain} onChange={e => setDomain(e.target.value)}
              placeholder="acmecorp.com" style={_SSO_INPUT_STYLE} />
            <small style={{ color:'var(--text3)' }}>
              Users on <code>*@&lt;domain&gt;</code> are forced through this provider — password login is refused for those addresses.
            </small>
          </label>
          <label style={_SSO_LABEL_STYLE}>
            DEFAULT GROUP (optional)
            <select value={defaultGroup} onChange={e => setDefaultGroup(e.target.value)} style={_SSO_INPUT_STYLE}>
              <option value="">— no auto-enrolment —</option>
              {(groups || []).map(g => (
                <option key={g.id} value={g.id}>{g.name}</option>
              ))}
            </select>
            <small style={{ color:'var(--text3)' }}>
              Newly JIT-provisioned users land in this group. Leave blank to require manual enrolment.
            </small>
          </label>
          {error && <div style={{ color:'#d13644', fontSize:11 }}>{error}</div>}
          <div style={{ display:'flex', gap:8 }}>
            <button type="button" onClick={onBack} style={_ssoBtn()}>CANCEL</button>
            <button type="submit" disabled={busy} style={_ssoBtn('primary')}>
              {busy ? 'CREATING…' : 'CREATE'}
            </button>
          </div>
        </form>
      </div>
    </>
  );
}

function AdminSSOProvidersPage({ density, navigate }) {
  const [reloadKey, setReloadKey] = useState(0);
  const [editing, setEditing]     = useState(null); // null | 'new' | row
  const [groups, setGroups]       = useState([]);
  const [secretFor, setSecretFor] = useState(null); // provider just created

  // Load groups once for the modal's dropdown. Per the iam REST surface
  // this requires groups:read — admins have *:* so this always succeeds
  // for the operators who can see this page. Failure here is non-fatal:
  // the modal still works without a group dropdown population.
  useEffect(() => {
    let cancelled = false;
    apiFetch('identity', '/v1/identity/groups')
      .then(g => { if (!cancelled) setGroups(g || []); })
      .catch(() => { /* leave empty; modal degrades gracefully */ });
    return () => { cancelled = true; };
  }, []);

  const loader = useCallback(
    () => apiFetch('identity', '/v1/identity/sso/providers'),
    [reloadKey],
  );

  const remove = useCallback(async (row) => {
    const ok = window.confirm(
      `Delete SSO provider?\n\n` +
      `  name:   ${row.name}\n` +
      `  kind:   ${row.kind}\n` +
      `  issuer: ${row.issuer || '(saml)'}\n\n` +
      `This is irreversible. Existing JIT-provisioned users keep ` +
      `their accounts but lose this login path; any user on the ` +
      `provider's domain_claim falls back to password login.`
    );
    if (!ok) return;
    try {
      await apiFetch('identity', `/v1/identity/sso/providers/${row.id}`, { method:'DELETE' });
      setReloadKey(k => k + 1);
    } catch (err) {
      window.alert(`Delete failed: ${err.message || err}`);
    }
  }, []);

  const columns = useMemo(() => ([
    ...SSO_PROVIDER_COLUMNS,
    { key:'_actions', label:'', width:140, render: (row) => (
        <span style={{ display:'inline-flex', gap:6 }}>
          <button onClick={() => setEditing(row)} style={_ssoBtn('row')}>EDIT</button>
          <button onClick={() => remove(row)}    style={_ssoBtn('danger')}>DELETE</button>
        </span>
      ),
    },
  ]), [remove]);

  const visibleWithActions = useMemo(() => {
    const s = new Set(SSO_PROVIDER_DEFAULT_VISIBLE);
    s.add('_actions');
    return s;
  }, []);

  const createButton = (
    <button onClick={() => navigate('admin', 'sso', 'new')} style={_ssoBtn('primary')}>
      + CREATE
    </button>
  );

  const breadcrumbs = (
    <Breadcrumbs crumbs={[{ label:'Identity' }, { label:'Identity Providers' }]} />
  );

  return (
    <>
      {breadcrumbs}
      {secretFor && (
        <SecretSnippetCallout provider={secretFor} onDismiss={() => setSecretFor(null)} />
      )}
      <DataTable loader={loader} columns={columns} navigate={navigate}
        lockedCols={SSO_PROVIDER_LOCKED_COLS} defaultVisible={visibleWithActions}
        emptyMessage="NO SSO PROVIDERS — CREATE ONE TO GET STARTED"
        density={density}
        labelCategories={[
          { key:'kind', label:'Kind', colors: SSO_KIND_COLOR },
        ]}
        actions={_bulkDeleteActions('iam', id => `/v1/identity/sso/providers/${id}`)}
        toolbarRight={createButton} />
      {editing && (
        <CreateOrEditProviderModal
          initial={editing === 'new' ? null : editing}
          groups={groups}
          onClose={() => setEditing(null)}
          onSaved={(saved, wasCreate) => {
            setEditing(null);
            setReloadKey(k => k + 1);
            if (wasCreate) setSecretFor(saved);
          }} />
      )}
    </>
  );
}
