/**
 * configure-login/generate.ts — Pure builders (no I/O).
 *
 * Split out so every transform is unit-testable in isolation:
 *   - buildBackendPatches : appsettings patch, partitioned into committed (no
 *     secrets in local-file mode) + local (secrets only).
 *   - buildAuthLiteral    : the `auth: {...}` object literal text for main.tsx.
 *   - injectProviderAuth  : idempotent, marker-based insertion into main.tsx.
 *   - buildEnvAppend      : append-only MSAL placeholders for .env.example.
 *   - generate            : assembles the JSON appsettings GeneratedFile[].
 *
 * index.ts owns all disk I/O and the deep-merge-on-write (via lib/json-merge).
 */
import type { ConfigureLoginInput, GeneratedFile, ProjectLayout } from './types.js';

type Json = Record<string, unknown>;

// ─────────────────────────────────────────────────────────────────────────────
// Backend appsettings patch (secret-safe partition)
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Build the appsettings patch, partitioned by secrecy.
 *  - `committed` is deep-merged into appsettings.json (committed). In 'local-file'
 *    mode it carries NO secrets (ClientSecret / InitialAdmin.Password are omitted,
 *    so the existing empty stubs survive). In 'placeholders' mode secrets are
 *    written as "" (still nothing real committed).
 *  - `local` is deep-merged into appsettings.Local.json (gitignored) and carries
 *    only the secret values. `null` when there is nothing secret to write.
 */
export function buildBackendPatches(spec: ConfigureLoginInput): { committed: Json; local: Json | null } {
  const committed: Json = {};
  const local: Json = {};
  const ct = spec.connectionTypes;
  const placeholders = spec.secretsMode === 'placeholders';

  // Authentication — Microsoft / Google OAuth.
  const authCommitted: Json = {};
  const authLocal: Json = {};
  const addProvider = (
    name: 'Microsoft' | 'Google',
    creds?: { clientId: string; clientSecret: string; secretExpiresAt?: string },
  ): void => {
    if (!creds) return;
    const pub: Json = { ClientId: creds.clientId };
    if (creds.secretExpiresAt) pub.SecretExpiresAt = creds.secretExpiresAt;
    if (placeholders) {
      authCommitted[name] = { ...pub, ClientSecret: '' };
    } else {
      authCommitted[name] = pub; // ClientSecret omitted → existing "" stub preserved
      authLocal[name] = { ClientSecret: creds.clientSecret };
    }
  };
  addProvider('Microsoft', ct.microsoft);
  addProvider('Google', ct.google);

  // EntraSso is bound at Authentication:EntraSso (EntraSsoSettings.SectionName),
  // so it lives UNDER Authentication, not at the top level. clientId is public
  // (the SPA app registration id), no secret. The accepted-tenant whitelist is
  // NOT stored here: EntraSsoSettings dropped AllowedTenantIds — it moved to
  // Authentication:Microsoft:AllowedTenants (shared with social Microsoft login,
  // see MicrosoftTenantPolicy), so we route it there instead.
  if (ct.entra) {
    const entra: Json = { Enabled: ct.entra.enabled, ClientId: ct.entra.clientId };
    if (ct.entra.authority) entra.Authority = ct.entra.authority;
    authCommitted.EntraSso = entra;
    if (ct.entra.allowedTenantIds) {
      const microsoft = (authCommitted.Microsoft as Json | undefined) ?? {};
      microsoft.AllowedTenants = ct.entra.allowedTenantIds;
      authCommitted.Microsoft = microsoft;
    }
  }

  if (Object.keys(authCommitted).length > 0) committed.Authentication = authCommitted;
  if (Object.keys(authLocal).length > 0) local.Authentication = authLocal;

  // Security — InitialAdmin only. There is deliberately NO EnableSelfRegistration
  // key: the backend (SecuritySettings) has no such flag and /api/auth/register
  // is always open (limited only by the license seat quota). Writing it would be
  // a dead, misleading key. The Security section is emitted only when an initial
  // admin is supplied. (The default seeded admin makes the API fail-fast on first
  // boot until Security:InitialAdmin:Password is set — see index.ts nextSteps.)
  if (spec.initialAdmin) {
    const admin: Json = { Email: spec.initialAdmin.email };
    if (spec.initialAdmin.requirePasswordChange !== undefined) {
      admin.RequirePasswordChange = spec.initialAdmin.requirePasswordChange;
    }
    if (placeholders) {
      admin.Password = '';
    } else {
      local.Security = { InitialAdmin: { Password: spec.initialAdmin.password } };
    }
    committed.Security = { InitialAdmin: admin };
  }

  // Email — sender + the chosen provider's settings block. Without the provider
  // block no mail is ever sent (the framework needs the host / api-key / conn
  // string). Provider secrets (SMTP password, SendGrid key, ACS connection
  // string) are partitioned into `local`; public fields stay committed.
  if (spec.email) {
    const e = spec.email;
    const email: Json = {
      Enabled: e.enabled,
      Provider: e.provider,
      FromEmail: e.fromEmail,
      FromName: e.fromName,
    };
    const emailLocal: Json = {};

    if (e.smtp) {
      const smtp: Json = { Host: e.smtp.host };
      if (e.smtp.port !== undefined) smtp.Port = e.smtp.port;
      if (e.smtp.username !== undefined) smtp.Username = e.smtp.username;
      if (e.smtp.useSsl !== undefined) smtp.UseSsl = e.smtp.useSsl;
      if (e.smtp.senderAddress !== undefined) smtp.SenderAddress = e.smtp.senderAddress;
      if (e.smtp.password !== undefined) {
        if (placeholders) smtp.Password = '';
        else emailLocal.Smtp = { Password: e.smtp.password };
      }
      email.Smtp = smtp;
    }
    if (e.sendGrid) {
      if (placeholders) email.SendGrid = { ApiKey: '' };
      else emailLocal.SendGrid = { ApiKey: e.sendGrid.apiKey };
    }
    if (e.azureAcs) {
      const acs: Json = { SenderAddress: e.azureAcs.senderAddress };
      if (placeholders) acs.ConnectionString = '';
      else emailLocal.AzureAcs = { ConnectionString: e.azureAcs.connectionString };
      email.AzureAcs = acs;
    }

    committed.Email = email;
    if (Object.keys(emailLocal).length > 0) local.Email = emailLocal;
  }

  return { committed, local: Object.keys(local).length > 0 ? local : null };
}

// ─────────────────────────────────────────────────────────────────────────────
// Frontend SmartStackProvider config.auth injection
// ─────────────────────────────────────────────────────────────────────────────

const AUTH_START = '/* @login-config:auth */';
const AUTH_END = '/* @end:login-config:auth */';
const AUTH_BLOCK_RE = /\/\* @login-config:auth \*\/[\s\S]*?\/\* @end:login-config:auth \*\//;

export type InjectStatus = 'injected' | 'updated' | 'skipped-customised' | 'not-found';

function isCustomised(source: string): boolean {
  const head = source.trimStart().slice(0, 100);
  return head.startsWith('/* @customised') || head.startsWith('// @customised');
}

/** The `auth: {...}` object literal text injected into the provider config. */
export function buildAuthLiteral(spec: ConfigureLoginInput): string {
  const parts: string[] = [];

  if (spec.connectionTypes.entra) {
    const e = spec.connectionTypes.entra;
    const entraParts = [`clientId: ${JSON.stringify(e.clientId)}`];
    if (e.authority) entraParts.push(`authority: ${JSON.stringify(e.authority)}`);
    parts.push(`entra: { ${entraParts.join(', ')} }`);
  }

  parts.push(`allowRegistration: ${spec.allowRegistration}`);

  if (spec.branding) {
    const b = spec.branding;
    const bParts: string[] = [];
    if (b.appName) bParts.push(`appName: ${JSON.stringify(b.appName)}`);
    if (b.logoUrl) bParts.push(`logoUrl: ${JSON.stringify(b.logoUrl)}`);
    if (b.heroTitle) bParts.push(`heroTitle: ${JSON.stringify(b.heroTitle)}`);
    if (b.heroSubtitle) bParts.push(`heroSubtitle: ${JSON.stringify(b.heroSubtitle)}`);
    if (bParts.length) parts.push(`branding: { ${bParts.join(', ')} }`);
  }

  return `auth: { ${parts.join(', ')} }`;
}

/**
 * Idempotently insert/refresh the `auth` property inside the SmartStackProvider
 * `config={{ … }}`. Re-runs replace the marked block in place (never duplicate).
 * Returns the (possibly unchanged) content plus a status the caller maps to
 * warnings/nextSteps.
 */
export function injectProviderAuth(
  source: string,
  spec: ConfigureLoginInput,
): { content: string; status: InjectStatus } {
  if (isCustomised(source)) return { content: source, status: 'skipped-customised' };

  const block = `${AUTH_START} ${buildAuthLiteral(spec)}, ${AUTH_END}`;

  if (AUTH_BLOCK_RE.test(source)) {
    return { content: source.replace(AUTH_BLOCK_RE, block), status: 'updated' };
  }

  const m = /config=\{\{/.exec(source);
  if (!m) return { content: source, status: 'not-found' };

  const at = m.index + m[0].length;
  return { content: source.slice(0, at) + block + source.slice(at), status: 'injected' };
}

// ─────────────────────────────────────────────────────────────────────────────
// .env.example MSAL placeholders (documentation only)
// ─────────────────────────────────────────────────────────────────────────────

/** Append `# VITE_MSAL_*` placeholders that are not already present (Entra only). */
export function buildEnvAppend(
  existing: string,
  spec: ConfigureLoginInput,
): { content: string; added: string[] } {
  if (!spec.connectionTypes.entra) return { content: existing, added: [] };

  const keys = ['VITE_MSAL_CLIENT_ID', 'VITE_MSAL_AUTHORITY', 'VITE_MSAL_REDIRECT_URI'];
  const added: string[] = [];
  const lines: string[] = [];
  for (const k of keys) {
    if (!new RegExp(`(^|\\n)#?\\s*${k}=`).test(existing)) {
      lines.push(`# ${k}=`);
      added.push(k);
    }
  }
  if (lines.length === 0) return { content: existing, added: [] };

  const sep = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
  const header = '\n# --- Entra SSO (MSAL) — added by login-config ---\n';
  return { content: existing + sep + header + lines.join('\n') + '\n', added };
}

// ─────────────────────────────────────────────────────────────────────────────
// File assembly
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Pure: build the JSON appsettings GeneratedFile[] (paths relative to
 * projectPath, POSIX slashes). The frontend injection + .env.example are handled
 * in index.ts because they transform existing on-disk content.
 */
export function generate(spec: ConfigureLoginInput, layout: ProjectLayout): GeneratedFile[] {
  const apiDir = layout.apiDir.replace(/\\/g, '/').replace(/\/+$/, '');
  const { committed, local } = buildBackendPatches(spec);

  const files: GeneratedFile[] = [
    {
      path: `${apiDir}/appsettings.json`,
      content: JSON.stringify(committed, null, 2) + '\n',
      strategy: 'deep-merge-json',
    },
  ];

  if (local) {
    files.push({
      path: `${apiDir}/appsettings.Local.json`,
      content: JSON.stringify(local, null, 2) + '\n',
      strategy: 'deep-merge-json',
    });
  }

  return files;
}
