/**
 * Auth routes — fastkit auth module.
 *
 * POST /api/auth/register        { email, password, name }
 * POST /api/auth/login           { email, password }
 * POST /api/auth/google          { idToken }  (Google Identity Services credential)
 * POST /api/auth/refresh         (refresh cookie or mobile refreshToken) -> rotates session
 * POST /api/auth/logout          revokes refresh token + clears cookie
 * GET  /api/auth/me              (Bearer) -> current user
 * GET  /api/auth/config          -> { googleClientId } for the frontend button
 * POST /api/auth/forgot-password { email }   (always 200 — no account enumeration)
 * POST /api/auth/reset-password  { token, password }
 *
 * Access token: 15 min JWT (Authorization: Bearer).
 * Refresh token: opaque, hashed in DB, rotated on use, httpOnly cookie.
 */
import { Router, type Request, type Response } from 'express';
import { eq, and, isNull } from 'drizzle-orm';
import { db } from '../db/index.js';
import { users, refreshTokens, passwordResetTokens, toPublicUser } from '../../shared/schema.js';
import { signAccessToken, generateOpaqueToken, hashToken } from '../lib/auth/jwt.js';
import { hashPassword, verifyPassword } from '../lib/auth/password.js';
import { requireAuth } from '../lib/auth/middleware.js';
import { sendEmail, templates } from '../lib/email/index.js';

const router = Router();

const REFRESH_COOKIE = 'fk_refresh';
const REFRESH_TTL_DAYS = 30;
const RESET_TTL_MINUTES = 30;

function appUrl(): string {
  return process.env.APP_URL || 'http://localhost:5173';
}

function requireDb() {
  if (!db) throw new Error('DATABASE_URL is not configured — auth requires a database.');
  return db;
}

function readCookie(req: Request, name: string): string | undefined {
  const raw = req.headers.cookie;
  if (!raw) return undefined;
  for (const part of raw.split(';')) {
    const [k, ...v] = part.trim().split('=');
    if (k === name) {
      try {
        return decodeURIComponent(v.join('='));
      } catch {
        // Cookies are untrusted request input. A stale or manually corrupted
        // percent-encoding must expire the session, never crash the API process.
        return undefined;
      }
    }
  }
  return undefined;
}

function readRefreshToken(req: Request): string | undefined {
  const bodyToken = (req.body as { refreshToken?: unknown } | undefined)?.refreshToken;
  const headerToken = req.headers['x-refresh-token'];
  return readCookie(req, REFRESH_COOKIE) ||
    (typeof bodyToken === 'string' ? bodyToken : undefined) ||
    (typeof headerToken === 'string' ? headerToken : undefined);
}

/**
 * The app is frequently embedded in a *cross-origin* preview iframe (e.g. the
 * editor at nstantcode.com framing a *.webprev.live deployment). In that
 * context a `SameSite=Lax` cookie is treated as third-party and silently
 * dropped by the browser, so the session is lost on every reload. Serving the
 * refresh cookie as `SameSite=None; Secure` (plus `Partitioned`/CHIPS so it
 * keeps working as browsers phase out third-party cookies) lets it survive both
 * top-level and inside a cross-site iframe.
 *
 * `SameSite=None` REQUIRES `Secure`, which only works over HTTPS — so locally we
 * fall back to `Lax` to keep http://localhost working. We treat the request as
 * cross-site capable when it arrived over HTTPS (directly or via a TLS-
 * terminating proxy) or when running in production.
 */
function isHttps(req: Request): boolean {
  return req.secure || req.headers['x-forwarded-proto'] === 'https' || process.env.NODE_ENV === 'production';
}

function refreshCookieOptions(req: Request) {
  const crossSite = isHttps(req);
  return {
    httpOnly: true,
    path: '/api/auth',
    sameSite: crossSite ? ('none' as const) : ('lax' as const),
    secure: crossSite,
    partitioned: crossSite,
  };
}

function setRefreshCookie(req: Request, res: Response, token: string): void {
  res.cookie(REFRESH_COOKIE, token, { ...refreshCookieOptions(req), maxAge: REFRESH_TTL_DAYS * 86400_000 });
}

function clearRefreshCookie(req: Request, res: Response): void {
  res.clearCookie(REFRESH_COOKIE, refreshCookieOptions(req));
}

async function issueSession(res: Response, user: typeof users.$inferSelect, req: Request) {
  const d = requireDb();
  const refresh = generateOpaqueToken();
  await d.insert(refreshTokens).values({
    userId: user.id,
    tokenHash: hashToken(refresh),
    userAgent: req.headers['user-agent']?.slice(0, 255) ?? null,
    expiresAt: new Date(Date.now() + REFRESH_TTL_DAYS * 86400_000),
  });
  setRefreshCookie(req, res, refresh);
  const accessToken = await signAccessToken({ sub: String(user.id), email: user.email, role: user.role });
  // Browsers receive only the httpOnly cookie. Native clients explicitly opt
  // in and store the opaque token in the platform secure store.
  return {
    accessToken,
    ...(req.headers['x-fastkit-client'] === 'mobile' ? { refreshToken: refresh } : {}),
    user: toPublicUser(user),
  };
}

// ---------- Register ----------
router.post('/register', async (req, res) => {
  const { email, password, name } = req.body ?? {};
  if (typeof email !== 'string' || !/^\S+@\S+\.\S+$/.test(email)) return res.status(400).json({ error: 'Valid email required' });
  if (typeof password !== 'string' || password.length < 8) return res.status(400).json({ error: 'Password must be at least 8 characters' });
  if (typeof name !== 'string' || name.trim().length < 1) return res.status(400).json({ error: 'Name required' });

  const d = requireDb();
  const normalized = email.trim().toLowerCase();
  const existing = await d.select().from(users).where(eq(users.email, normalized)).limit(1);
  if (existing.length > 0) return res.status(409).json({ error: 'An account with this email already exists' });

  const [user] = await d
    .insert(users)
    .values({ email: normalized, name: name.trim(), passwordHash: hashPassword(password) })
    .returning();

  // Fire-and-forget welcome email
  const tpl = templates.welcome({ name: user.name, loginUrl: appUrl() });
  void sendEmail({ to: user.email, ...tpl }).catch(() => {});

  res.status(201).json(await issueSession(res, user, req));
});

// ---------- Login ----------
router.post('/login', async (req, res) => {
  const { email, password } = req.body ?? {};
  if (typeof email !== 'string' || typeof password !== 'string') return res.status(400).json({ error: 'Email and password required' });

  const d = requireDb();
  const [user] = await d.select().from(users).where(eq(users.email, email.trim().toLowerCase())).limit(1);
  if (!user?.passwordHash || !verifyPassword(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Incorrect email or password' });
  }
  res.json(await issueSession(res, user, req));
});

// ---------- Google sign-in ----------
router.post('/google', async (req, res) => {
  const { idToken } = req.body ?? {};
  const clientId = process.env.GOOGLE_CLIENT_ID;
  if (!clientId) return res.status(501).json({ error: 'Google sign-in is not configured' });
  if (typeof idToken !== 'string') return res.status(400).json({ error: 'idToken required' });

  // Verify the ID token against Google
  const verifyRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(idToken)}`);
  if (!verifyRes.ok) return res.status(401).json({ error: 'Google sign-in failed' });
  const info = (await verifyRes.json()) as { aud?: string; sub?: string; email?: string; name?: string; picture?: string; email_verified?: string };
  if (info.aud !== clientId || !info.sub || !info.email) return res.status(401).json({ error: 'Google sign-in failed' });

  const d = requireDb();
  const normalized = info.email.toLowerCase();
  let [user] = await d.select().from(users).where(eq(users.googleId, info.sub)).limit(1);
  if (!user) {
    // Link to an existing email account, or create a new one
    const [byEmail] = await d.select().from(users).where(eq(users.email, normalized)).limit(1);
    if (byEmail) {
      [user] = await d.update(users).set({ googleId: info.sub, avatarUrl: byEmail.avatarUrl ?? info.picture ?? null }).where(eq(users.id, byEmail.id)).returning();
    } else {
      [user] = await d.insert(users).values({ email: normalized, name: info.name || normalized.split('@')[0], googleId: info.sub, avatarUrl: info.picture ?? null }).returning();
    }
  }
  res.json(await issueSession(res, user, req));
});

// ---------- Refresh (rotation) ----------
router.post('/refresh', async (req, res) => {
  const token = readRefreshToken(req);
  if (!token) {
    // Also removes malformed/stale browser cookies so the next reload starts clean.
    clearRefreshCookie(req, res);
    return res.status(401).json({ error: 'No session' });
  }

  const d = requireDb();
  const [row] = await d
    .select()
    .from(refreshTokens)
    .where(and(eq(refreshTokens.tokenHash, hashToken(token)), isNull(refreshTokens.revokedAt)))
    .limit(1);
  if (!row || row.expiresAt < new Date()) {
    clearRefreshCookie(req, res);
    return res.status(401).json({ error: 'Session expired' });
  }

  const [user] = await d.select().from(users).where(eq(users.id, row.userId)).limit(1);
  if (!user) {
    clearRefreshCookie(req, res);
    return res.status(401).json({ error: 'Session expired' });
  }

  // Rotate: revoke old, issue new
  await d.update(refreshTokens).set({ revokedAt: new Date() }).where(eq(refreshTokens.id, row.id));
  res.json(await issueSession(res, user, req));
});

// ---------- Logout ----------
router.post('/logout', async (req, res) => {
  const token = readRefreshToken(req);
  if (token && db) {
    await db.update(refreshTokens).set({ revokedAt: new Date() }).where(eq(refreshTokens.tokenHash, hashToken(token)));
  }
  clearRefreshCookie(req, res);
  res.json({ ok: true });
});

// ---------- Me ----------
router.get('/me', requireAuth, async (req, res) => {
  const d = requireDb();
  const [user] = await d.select().from(users).where(eq(users.id, req.user!.id)).limit(1);
  if (!user) return res.status(404).json({ error: 'Account not found' });
  res.json({ user: toPublicUser(user) });
});

// ---------- Frontend config ----------
router.get('/config', (_req, res) => {
  res.json({ googleClientId: process.env.GOOGLE_CLIENT_ID || null });
});

// ---------- Forgot password ----------
router.post('/forgot-password', async (req, res) => {
  const { email } = req.body ?? {};
  // Always answer 200 to prevent account enumeration.
  res.json({ ok: true });
  if (typeof email !== 'string' || !db) return;

  const [user] = await db.select().from(users).where(eq(users.email, email.trim().toLowerCase())).limit(1);
  if (!user) return;

  const token = generateOpaqueToken();
  await db.insert(passwordResetTokens).values({
    userId: user.id,
    tokenHash: hashToken(token),
    expiresAt: new Date(Date.now() + RESET_TTL_MINUTES * 60_000),
  });
  const tpl = templates.passwordReset({
    name: user.name,
    resetUrl: `${appUrl()}/reset-password?token=${token}`,
    expiresMinutes: RESET_TTL_MINUTES,
  });
  void sendEmail({ to: user.email, ...tpl }).catch(() => {});
});

// ---------- Reset password ----------
router.post('/reset-password', async (req, res) => {
  const { token, password } = req.body ?? {};
  if (typeof token !== 'string' || typeof password !== 'string' || password.length < 8) {
    return res.status(400).json({ error: 'Invalid token or password too short (min 8 characters)' });
  }
  const d = requireDb();
  const [row] = await d
    .select()
    .from(passwordResetTokens)
    .where(and(eq(passwordResetTokens.tokenHash, hashToken(token)), isNull(passwordResetTokens.usedAt)))
    .limit(1);
  if (!row || row.expiresAt < new Date()) return res.status(400).json({ error: 'This reset link is invalid or has expired' });

  await d.update(passwordResetTokens).set({ usedAt: new Date() }).where(eq(passwordResetTokens.id, row.id));
  await d.update(users).set({ passwordHash: hashPassword(password) }).where(eq(users.id, row.userId));
  // Revoke all sessions for safety
  await d.update(refreshTokens).set({ revokedAt: new Date() }).where(and(eq(refreshTokens.userId, row.userId), isNull(refreshTokens.revokedAt)));
  res.json({ ok: true });
});

export default router;
