import express, { Request, Response } from 'express';
import { Repository } from '../db/repository.js';
import { logger } from '../logger.js';
import { loadConfig } from '../config.js';
import type { AuthConfig } from '../config.js';
import { ConfigManager } from '../config-manager.js';
import { resolveBranding } from './branding-api.js';
import {
  setupAuth,
  requireAuth,
  requireAdmin,
  isProviderConfigured,
  isLocalEnabled,
  buildChangePasswordHandler,
  DEFAULT_SESSION_SECRET_PATH,
} from './auth.js';

/**
 * Auth activation + middleware pipeline, extracted verbatim from
 * createCoreServer (bridge/server.ts). Two entry points:
 *
 * - resolveAuthActivation: decides whether auth is active (fail-closed on a
 *   partial OAuth config) and seeds the synthetic 'local' user in no-auth mode.
 * - mountAuthPipeline: registers session/passport middleware, /auth routes,
 *   /api/auth/* endpoints and the requireAuth/requireAdmin guards for the
 *   /api/* namespaces. Call ONLY when auth is active — registration order
 *   relative to all later mounts is preserved by the call site in server.ts.
 */

/** Cookie `secure` must be set whenever the user-facing scheme is https —
 *  via an upstream TLS proxy (secureCookie) OR native TLS termination. */
export function computeEffectiveSecureCookie(secureCookie: boolean, tlsEnabled: boolean): boolean {
  return secureCookie || tlsEnabled;
}

export function resolveAuthActivation(
  repo: Repository,
  authConfig: AuthConfig | undefined,
): { authActive: boolean; localEnabled: boolean } {
  // Auth activates when a provider is COMPLETELY configured (clientId +
  // clientSecret + callbackUrl, plus baseUrl for Gitea).
  //
  // FAIL CLOSED on a partial config: if the operator clearly INTENDED auth
  // (a provider has a client_id) but it's incomplete (typo'd / missing
  // secret/callback), we must NOT silently fall back to no-auth — that would
  // fail OPEN and expose /api/local, /api/config, etc. without authentication.
  // Refuse to start instead, with a clear message. (A bare `auth` block with no
  // client_id at all is treated as genuine no-auth mode.)
  const _authProviders = authConfig?.providers;
  const authUsable =
    isProviderConfigured(_authProviders?.google, 'google') ||
    isProviderConfigured(_authProviders?.gitea, 'gitea');
  // "Intended" = ANY OAuth field is present on a provider (not just client_id).
  // Saving only client_secret/callback_url/base_url, or clearing client_id by
  // mistake, must still fail closed rather than drop to no-auth.
  const _hasAnyField = (p?: { clientId?: string; clientSecret?: string; callbackUrl?: string; baseUrl?: string }) =>
    !!(p?.clientId || p?.clientSecret || p?.callbackUrl || p?.baseUrl);
  const authIntended = _hasAnyField(_authProviders?.google) || _hasAnyField(_authProviders?.gitea);
  // Local email+password is a first-class auth mode: when enabled, auth is
  // active even without any OAuth provider.
  const localEnabled = !!authConfig && isLocalEnabled(authConfig);
  // Fail closed on a partial OAuth config ONLY when it would otherwise drop to
  // no-auth. If local auth is on, auth is already active (no fail-open), so a
  // half-configured OAuth provider just stays inactive rather than aborting boot.
  if (authIntended && !authUsable && !localEnabled) {
    throw new Error(
      '[auth] auth is partially configured: a provider has a client_id but is missing ' +
        'client_secret / callback_url (Gitea also needs base_url). Refusing to start in an ' +
        'insecure no-auth state — complete the provider config or remove it from config.yaml.',
    );
  }
  const authActive = authUsable || localEnabled;
  if (!authActive) {
    // No-auth single-user mode: per-user rows are owned by the synthetic
    // 'local' id, and several tables FK to users(id) (ssh_user_deks,
    // browser_session_profiles, …). Seed the 'local' user row so those inserts
    // succeed — without it, creating an SSH connection failed with create_failed.
    repo.ensureLocalUser();
  }
  return { authActive, localEnabled };
}

export function mountAuthPipeline(
  app: express.Application,
  deps: {
    repo: Repository;
    authConfig: AuthConfig | undefined;
    configManager?: ConfigManager;
    localEnabled: boolean;
    /** serverCfg.tls.enabled — resolved once in createCoreServer. */
    tlsEnabled: boolean;
  },
): import('./auth.js').UpgradeAuthChecker | undefined {
  const { repo, authConfig, configManager, localEnabled, tlsEnabled } = deps;

  // Idempotently seed the shared `local` system admin (id='local', the same
  // owner the no-auth path uses) so an existing single-user / no-auth
  // deployment gains a login mid-stream and keeps its `local`-owned data.
  const bootstrap = authConfig?.local?.bootstrapAdmin;
  if (localEnabled && bootstrap?.email && bootstrap?.password) {
    repo.upsertLocalSystemAdmin({ email: bootstrap.email, password: bootstrap.password });
    logger.info(`[auth] seeded local system admin id=local email=${bootstrap.email}`);
  }

  // Compose effective secureCookie: native TLS termination also requires
  // the secure flag on session cookies, even when no upstream proxy is
  // present. IMPORTANT: trust-proxy (set near the top of createCoreServer)
  // stays keyed on the ORIGINAL opts.authConfig.secureCookie — native TLS
  // must NOT enable it.
  const effectiveSecureCookie = computeEffectiveSecureCookie(
    !!authConfig?.secureCookie,
    tlsEnabled,
  );
  const authConfigForSetup = authConfig
    ? { ...authConfig, secureCookie: effectiveSecureCookie }
    : authConfig;

  const auth = setupAuth(
    repo,
    authConfigForSetup!,
    () => {
      const b = resolveBranding(configManager);
      return { appName: b.appName, loginPageTitle: b.loginPageTitle };
    },
    loadConfig()?.secrets?.sessionSecretPath ?? DEFAULT_SESSION_SECRET_PATH,
  );
  const { sessionMiddleware, passportInit, passportSession, authRouter } = auth;
  const authenticateUpgrade = auth.authenticateUpgrade;

  // Global session + passport middleware (BEFORE all routes)
  app.use(sessionMiddleware);
  app.use(passportInit);
  app.use(passportSession);

  // Auth routes (unauthenticated access allowed)
  app.use('/auth', authRouter);

  // /api/auth/me endpoint
  app.get('/api/auth/me', requireAuth, (req: Request, res: Response) => {
    res.json(req.user);
  });

  // Self-service password change for local accounts (see auth.ts).
  app.post('/api/auth/password', requireAuth, express.json(), buildChangePasswordHandler(repo));

  // Protect all API routes (except /api/version and /health)
  app.use('/api/local', requireAuth);
  app.use('/api/repos', requireAuth);
  // /api/workers exposes endpoint URLs + proxy backend probing. Without
  // auth, unauthenticated callers could (a) enumerate worker endpoints
  // and (b) trigger upstream `/v1/models` fetches against any
  // attacker-influenced endpoint (SSRF amplifier). Gate behind requireAuth
  // — admin-only is unnecessary because the responses already strip
  // sensitive fields (apiKey), but anonymous access must be blocked.
  app.use('/api/workers', requireAuth);

  // Admin-only routes
  app.use('/api/config', requireAdmin);
  // /api/pieces: any authenticated user can GET (visibility-filtered);
  // per-piece write authz (built-in/global-custom → admin, user-custom → owner)
  // is enforced inside pieces-api.ts handlers.
  app.use('/api/pieces', requireAuth);
  app.use('/api/usage', requireAuth);
  // 横断カレンダー: 認証時はログイン必須。可視スペースの絞り込みは
  // Repository.getCrossSpaceCalendarMonth が viewer 単位で行う。
  app.use('/api/calendar', requireAuth);
  // Scheduled tasks: any authenticated user can create/list (visibility-filtered).
  // PATCH/DELETE owner-or-admin enforcement lives in the handlers (Task 14).
  app.use('/api/scheduled-tasks', requireAuth);

  return authenticateUpgrade;
}
