// api.ts から分割（挙動不変）: Config（設定 API + シークレット4状態表現）。
import { BASE } from './client';

/**
 * 4-state representation of a secret field on the wire. See design doc
 * 2026-05-21-settings-ui-and-config-restructure-design.md (Form Behavior →
 * Secret Inputs) for the canonical contract.
 *
 * Phase 1 (this release) keeps the on-wire representation backwards
 * compatible with the existing `apiKey: string` shape so that mask-
 * preservation in `ConfigManager.updateConfig` keeps working unchanged:
 *
 *   - `unchanged` → serialized as the masked sentinel `'********'`
 *   - `literal`   → serialized as the raw string value
 *   - `env_ref`   → serialized as `${ENV_NAME}`
 *   - `cleared`   → serialized as `''` (empty string)
 *
 * Phase 2 will switch to a tagged object on the wire and drop the magic
 * `'********'` sentinel; the UI form keeps the 4-state union today so the
 * Phase 2 migration is a server-side change only.
 */
export type SecretFieldValue =
  | { type: 'unchanged' }
  | { type: 'literal'; value: string }
  | { type: 'env_ref'; env_name: string }
  | { type: 'cleared' };

/** Server-side masked sentinel. Kept in sync with `MASKED` in src/config-manager.ts. */
export const SECRET_MASKED_SENTINEL = '********';

/**
 * Parse a stored string secret (as received from `GET /api/config`) into
 * the 4-state form used by the UI. The server masks literal secrets to
 * `'********'`, so any literal-looking string is treated as `unchanged`
 * unless it's an `${ENV_REF}` pattern. Empty / missing values map to
 * `cleared`.
 */
export function parseSecretValue(raw: string | null | undefined): SecretFieldValue {
  if (raw == null || raw === '') return { type: 'cleared' };
  if (raw === SECRET_MASKED_SENTINEL) return { type: 'unchanged' };
  const envMatch = /^\$\{([A-Z0-9_]+)\}$/.exec(raw.trim());
  if (envMatch) return { type: 'env_ref', env_name: envMatch[1] };
  // Anything else came back as plaintext (e.g. fresh UI form not yet
  // round-tripped through the server mask) — treat as literal.
  return { type: 'literal', value: raw };
}

/**
 * Serialize a 4-state secret into the string the server currently
 * expects. `unchanged` becomes the masked sentinel so server-side
 * mask-preservation in `ConfigManager.updateConfig` keeps the existing
 * literal in place.
 */
export function serializeSecretValue(v: SecretFieldValue): string {
  if (v.type === 'unchanged') return SECRET_MASKED_SENTINEL;
  if (v.type === 'literal') return v.value;
  if (v.type === 'env_ref') return `\${${v.env_name}}`;
  return '';
}

// --- Config ---
export async function fetchConfig(): Promise<{ config: any; etag: string; overriddenByEnv: Record<string, boolean> }> {
  const res = await fetch(`${BASE}/config`);
  const data = await res.json();
  if (!res.ok) throw new Error(data?.error ?? 'Failed to fetch config');
  return { config: data.config, etag: res.headers.get('etag') ?? '', overriddenByEnv: data.overriddenByEnv ?? {} };
}

export async function updateConfig(config: any, etag: string): Promise<{ ok: boolean; conflict?: boolean }> {
  const res = await fetch(`${BASE}/config`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json', 'If-Match': etag },
    body: JSON.stringify(config),
  });
  const data = await res.json();
  if (res.status === 409) return { ok: false, conflict: true };
  if (!res.ok) throw new Error(data?.error ?? 'Failed to update config');
  return data;
}

export async function reloadConfig(): Promise<void> {
  const res = await fetch(`${BASE}/config/reload`, { method: 'POST' });
  if (!res.ok) throw new Error('Failed to reload config');
}
