/**
 * configure-login/types.ts — Input contract + emitted file shape.
 *
 * This CLI configures the LOGIN/AUTH experience of a generated SmartStack client
 * app by WRITING CONFIGURATION the package reads. It does NOT scaffold a login
 * page (the UI ships in @atlashub/smartstack). Three channels:
 *   1. backend appsettings (deep-merged) — providers, registration flag, admin, email
 *   2. frontend SmartStackProvider `config.auth` (gated `frontendAuth`) — Entra clientId,
 *      allowRegistration, branding (needed because a pre-built package can't read VITE_*)
 *   3. optional .env.example MSAL placeholders (documentation only)
 */
import { z } from 'zod';

const ProviderCredsSchema = z.object({
  clientId: z.string().min(1),
  clientSecret: z.string().min(1),
  /** Optional ISO date — copied verbatim to `SecretExpiresAt` (used for expiry warnings). */
  secretExpiresAt: z.string().optional(),
});

const EntraSchema = z.object({
  enabled: z.boolean().default(true),
  clientId: z.string().min(1),
  authority: z.string().min(1).optional(),
  allowedTenantIds: z.array(z.string().min(1)).optional(),
});

const ConnectionTypesSchema = z
  .object({
    local: z.boolean(),
    microsoft: ProviderCredsSchema.optional(),
    google: ProviderCredsSchema.optional(),
    entra: EntraSchema.optional(),
  })
  .refine((c) => c.local || !!c.microsoft || !!c.google || !!c.entra, {
    message: 'At least one connection type must be enabled (local, microsoft, google, or entra).',
  });

const BrandingSchema = z.object({
  appName: z.string().min(1).optional(),
  logoUrl: z.string().min(1).optional(),
  heroTitle: z.string().optional(),
  heroSubtitle: z.string().optional(),
});

const InitialAdminSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1),
  /** Force a password change on the admin's first login (Security:InitialAdmin:RequirePasswordChange). */
  requirePasswordChange: z.boolean().optional(),
});

// Per-provider email credentials. The backend (EmailService.SelectProvider)
// matches `Provider` case-insensitively and silently falls back to the
// Development provider for any unknown value — so the provider is an enum, and
// the chosen provider's settings block must be present (refined below) or no
// mail is ever sent.
const SmtpSchema = z.object({
  host: z.string().min(1),
  port: z.number().int().positive().optional(),
  username: z.string().optional(),
  password: z.string().optional(), // secret → appsettings.Local.json
  useSsl: z.boolean().optional(),
  senderAddress: z.string().optional(),
});
const SendGridSchema = z.object({
  apiKey: z.string().min(1), // secret → appsettings.Local.json
});
const AzureAcsSchema = z.object({
  connectionString: z.string().min(1), // secret → appsettings.Local.json
  senderAddress: z.string().email(),
});

const EmailSchema = z
  .object({
    enabled: z.boolean().default(true),
    provider: z.enum(['Development', 'Smtp', 'SendGrid', 'AzureAcs']),
    fromEmail: z.string().email(),
    fromName: z.string().min(1),
    smtp: SmtpSchema.optional(),
    sendGrid: SendGridSchema.optional(),
    azureAcs: AzureAcsSchema.optional(),
  })
  .refine((e) => e.provider !== 'Smtp' || !!e.smtp?.host, {
    message: 'email.provider "Smtp" requires email.smtp.host (and usually username/password).',
    path: ['smtp'],
  })
  .refine((e) => e.provider !== 'SendGrid' || !!e.sendGrid?.apiKey, {
    message: 'email.provider "SendGrid" requires email.sendGrid.apiKey.',
    path: ['sendGrid'],
  })
  .refine((e) => e.provider !== 'AzureAcs' || !!e.azureAcs?.connectionString, {
    message: 'email.provider "AzureAcs" requires email.azureAcs.connectionString.',
    path: ['azureAcs'],
  });

export const ConfigureLoginInputSchema = z.object({
  projectPath: z.string().min(1),
  appCode: z.string().min(1),
  connectionTypes: ConnectionTypesSchema,
  /**
   * Whether self-registration is offered. NOTE: the backend has NO flag to
   * disable registration — POST /api/auth/register is always open (limited only
   * by the license seat quota). This is therefore NOT written to appsettings;
   * it is a UI signal used by a scaffolded custom login page
   * (scaffold-login-page) to show/hide the "create account" link, and by the
   * (gated, default-off) frontend config.auth literal.
   */
  allowRegistration: z.boolean(),
  branding: BrandingSchema.optional(),
  initialAdmin: InitialAdminSchema.optional(),
  email: EmailSchema.optional(),
  /**
   * 'local-file' (default): secrets → gitignored appsettings.Local.json; the
   *   committed appsettings.json keeps empty stubs.
   * 'placeholders': secrets written as "" into appsettings.json; nothing secret
   *   is persisted (user populates Local / user-secrets manually).
   */
  secretsMode: z.enum(['local-file', 'placeholders']).default('local-file'),
  /**
   * Inject `config.auth` into web/{appCode}-web/src/main.tsx. DEFAULT FALSE:
   * injecting before @atlashub/smartstack's SmartStackProvider type accepts
   * `auth` would break the client's TypeScript build. Enable once the package
   * exposes the contract.
   */
  frontendAuth: z.boolean().default(false),
  /** Append documentary `# VITE_MSAL_*` placeholders to .env.example (Entra only). */
  writeEnvExample: z.boolean().default(false),
  force: z.boolean().default(false),
});

export type ConfigureLoginInput = z.infer<typeof ConfigureLoginInputSchema>;

/** Filesystem layout resolved by validate() (relative to projectPath, POSIX slashes). */
export interface ProjectLayout {
  /** .NET API folder holding appsettings.json. */
  apiDir: string;
  /** Frontend folder holding src/main.tsx. */
  webDir: string;
}

export interface GeneratedFile {
  path: string;
  content: string;
  /** Only JSON appsettings files flow through generate(); both deep-merge. */
  strategy: 'overwrite' | 'deep-merge-json';
}
