import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';

import { db } from '@/lib/db';

/**
 * Better Auth server configuration.
 *
 * Sessions are stored in PostgreSQL (not Redis) per SECURITY-GUIDELINES.md §1.2.
 * The drizzle adapter handles session persistence via the existing `sessions` table.
 */
/**
 * In dev, allow requests from any LAN IP so the same `pnpm dev` instance can
 * serve both localhost (Mac) and the LAN IP (iPhone on Wi-Fi). In production,
 * trustedOrigins is locked down to NEXT_PUBLIC_APP_URL only.
 */
/**
 * In dev, allow localhost + any LAN-IP origin so the same `pnpm dev` instance
 * can serve both Mac (localhost) and iPhone-on-Wi-Fi (192.168.x.x). The
 * function form is preferred over a static list because the LAN IP can vary
 * across networks. In production, lock down to NEXT_PUBLIC_APP_URL only.
 */
const isProd = process.env.NODE_ENV === 'production';
const DEV_ORIGIN_PATTERNS = [
  /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/,
  /^https?:\/\/192\.168\.\d+\.\d+(:\d+)?$/,
  /^https?:\/\/10\.\d+\.\d+\.\d+(:\d+)?$/,
];

const trustedOrigins: (request?: Request) => Promise<string[]> = async (request) => {
  if (isProd) {
    const prodUrl = process.env.NEXT_PUBLIC_APP_URL;
    return prodUrl ? [prodUrl] : [];
  }
  const origin = request?.headers.get('origin') ?? '';
  if (origin && DEV_ORIGIN_PATTERNS.some((re) => re.test(origin))) {
    return [origin];
  }
  return ['http://localhost:3000', 'http://localhost:3001'];
};

/**
 * `betterAuth(...)` is wrapped in a lazy initializer so the auth singleton
 * is constructed on first property access (i.e. first request) rather than
 * at module import. This is required so that Next.js's "collect page data"
 * phase during `pnpm build` doesn't trigger better-auth's "default secret"
 * check against the unset BETTER_AUTH_SECRET - at build time the auth
 * config is never accessed, and at runtime the env is fully populated.
 *
 * Call sites continue to use `auth.api.foo(...)` unchanged; the Proxy
 * intercepts the property access and resolves the real instance just-in-
 * time. `typeof auth.$Infer.Session` is a type-only access and never
 * triggers the Proxy at runtime.
 */
function buildAuth() {
  return betterAuth({
    database: drizzleAdapter(db, {
      provider: 'pg',
    }),

    trustedOrigins,

    emailAndPassword: {
      enabled: true,
      minPasswordLength: 9,
      // Accounts are admin-created only - no self-service email verification flow.
      requireEmailVerification: false,
      // auth-flow-auditor C1: revoke every existing session for the user
      // when their password is reset. Without this, a stolen cookie
      // keeps working forever after the legitimate owner does the
      // forgot-password dance. Better Auth flips this internally; the
      // option below routes through `internalAdapter.deleteSessions(userId)`.
      revokeSessionsOnPasswordReset: true,
      // Self-service password reset for CRM users. The reset link lands
      // on the existing /reset-password page (which already handles
      // better-auth's token + new-password POST). The email send goes
      // through the shared SMTP infra so EMAIL_REDIRECT_TO honours it
      // in dev.
      sendResetPassword: async ({ user, url }) => {
        const [{ sendEmail }, { resolveAuthShellBranding }, { buildAccountPasswordEmail }] =
          await Promise.all([
            import('@/lib/email'),
            import('@/lib/email/auth-shell-branding'),
            import('@/lib/auth/account-setup-email'),
          ]);

        const branding = await resolveAuthShellBranding();
        const appName = branding?.appName ?? 'CRM';

        // Admin-created users ride the same reset-token machinery but should
        // receive a welcome email, not a "you requested a reset" one — the
        // create-user service marks them just before triggering this. The
        // builder picks welcome-vs-reset and renders accordingly.
        const mail = await buildAccountPasswordEmail({
          email: user.email,
          name: user.name,
          url,
          appName,
          authBranding: branding,
        });
        await sendEmail(user.email, mail.subject, mail.html, undefined, mail.text);
      },
    },

    session: {
      // Enable cookie-level session caching to reduce DB reads (5-minute cache).
      cookieCache: {
        enabled: true,
        maxAge: 5 * 60,
      },
      // Absolute session lifetime: 24 hours.
      expiresIn: 60 * 60 * 24,
      // Refresh the session whenever the user is active in the last 25% of its lifetime (6h).
      updateAge: 60 * 60 * 6,
    },

    advanced: {
      cookiePrefix: 'pn-crm',
      defaultCookieAttributes: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'strict' as const,
      },
      // Trust the X-Forwarded-For chain when running behind Caddy/Nginx in
      // production so the IP that rate-limit + audit logging see is the
      // real client, not the proxy. Skipped in dev (no proxy in front).
      ipAddress: {
        ipAddressHeaders: isProd ? ['x-forwarded-for', 'x-real-ip'] : [],
      },
    },

    // Rate limiting (post-audit F7) - without this, brute-force is wide
    // open. Tight caps on the credential-eating endpoints; loose default
    // for everything else so legitimate fan-out (multi-widget dashboards
    // that hit /get-session repeatedly) isn't hampered.
    rateLimit: {
      enabled: true,
      window: 60,
      max: 120,
      // Memory storage resets on restart. For multi-replica prod, swap
      // to `storage: 'database'` once the rateLimit migration is run.
      storage: 'memory',
      customRules: {
        '/sign-in/email': { window: 60, max: 5 },
        '/sign-up/email': { window: 60, max: 3 },
        '/forget-password': { window: 60, max: 3 },
        '/reset-password': { window: 60, max: 5 },
      },
    },

    logger: {
      disabled: false,
      level: 'error' as const,
    },
  });
}

type AuthInstance = ReturnType<typeof buildAuth>;

let _authInstance: AuthInstance | null = null;
function getAuth(): AuthInstance {
  if (!_authInstance) _authInstance = buildAuth();
  return _authInstance;
}

export const auth = new Proxy({} as AuthInstance, {
  get(_target, prop) {
    return Reflect.get(getAuth(), prop);
  },
  // Critical: better-auth's `toNextJsHandler` does `"handler" in auth`
  // and falls back to calling `auth(req)` if false. The default `has`
  // trap looks at the empty target (returning false), so without this
  // we'd hit that fallback (and crash because the target isn't
  // callable). Forward `in` checks to the real instance.
  has(_target, prop) {
    return Reflect.has(getAuth(), prop);
  },
});

export type Session = typeof auth.$Infer.Session;
export type User = typeof auth.$Infer.Session.user;
