import { createHash, randomBytes, randomUUID } from 'node:crypto';
import bcrypt from 'bcryptjs';
import { Router } from 'express';
import { CodeChallengeMethod, OAuth2Client } from 'google-auth-library';
import { z } from 'zod';
import { createToken } from './auth';
import { config } from './config';
import { pool, type DatabaseUser } from './db';
import { recordUserAccess } from './accessLogs';
import { createRateLimit } from './rateLimit';
import { generateAvailableUsername } from './usernames';

export const googleAuthRouter = Router();

const startLimit = createRateLimit({
  windowMs: 15 * 60_000,
  maxRequests: 30,
  message: 'Too many Google sign-in attempts. Try again later.',
});
const exchangeLimit = createRateLimit({
  windowMs: 15 * 60_000,
  maxRequests: 30,
  message: 'Too many Google sign-in exchanges. Try again later.',
});

const digest = (value: string): string => createHash('sha256').update(value).digest('hex');
const oauthClient = (): OAuth2Client => {
  if (!config.google.clientId || !config.google.clientSecret) {
    throw new Error('Google authentication is not configured');
  }
  return new OAuth2Client({
    clientId: config.google.clientId,
    clientSecret: config.google.clientSecret,
    redirectUri: config.google.callbackUrl,
  });
};

const callbackUrl = (base: string, key: 'google_code' | 'google_error', value: string): string => {
  const url = new URL(base);
  url.searchParams.set(key, value);
  return url.toString();
};

const publicUser = (user: DatabaseUser) => ({
  id: user.id,
  email: user.email,
  username: user.username,
  displayName: user.display_name,
  role: user.role,
  accountType: user.account_type,
});

const findOrCreateGoogleUser = async (subject: string, email: string, name: string): Promise<DatabaseUser> => {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [`google:${email}`]);
    const linked = await client.query<DatabaseUser>(
      `SELECT users.* FROM google_auth_identities identity
       JOIN users ON users.id=identity.user_id WHERE identity.provider_subject=$1`,
      [subject],
    );
    let user = linked.rows[0];
    let registered = false;
    if (!user) {
      const byEmail = await client.query<DatabaseUser>('SELECT * FROM users WHERE LOWER(email)=$1 LIMIT 1', [email]);
      user = byEmail.rows[0];
      if (!user) {
        const id = randomUUID();
        const displayName = name.trim().slice(0, 80) || email.split('@')[0].slice(0, 80);
        const username = await generateAvailableUsername(client, displayName);
        const passwordHash = await bcrypt.hash(randomBytes(48).toString('base64url'), 12);
        const created = await client.query<DatabaseUser>(
          `INSERT INTO users (id,email,password_hash,display_name,username,role)
           VALUES ($1,$2,$3,$4,$5,'user') RETURNING *`,
          [id, email, passwordHash, displayName, username],
        );
        user = created.rows[0];
        await client.query('INSERT INTO profiles (user_id,display_name) VALUES ($1,$2)', [id, displayName]);
        registered = true;
      }
      await client.query(
        `INSERT INTO google_auth_identities (provider_subject,user_id,email_at_link)
         VALUES ($1,$2,$3) ON CONFLICT (provider_subject) DO NOTHING`,
        [subject, user.id, email],
      );
    } else {
      await client.query(
        'UPDATE google_auth_identities SET last_login_at=NOW() WHERE provider_subject=$1',
        [subject],
      );
    }
    if (!user.is_active || 'deleted_at' in user && user.deleted_at) {
      throw new Error('Google account belongs to an unavailable application account');
    }
    if (registered) {
      await client.query(
        "INSERT INTO audit_events (user_id,event_type,details) VALUES ($1,'account_registered_google',$2::jsonb)",
        [user.id, JSON.stringify({ email })],
      );
    }
    await client.query('COMMIT');
    return user;
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
};

googleAuthRouter.get('/auth/google/status', (_request, response) => {
  response.setHeader('Cache-Control', 'no-store');
  response.json({ configured: config.google.configured });
});

googleAuthRouter.get('/auth/google/start', startLimit, async (request, response) => {
  if (!config.google.configured) {
    response.status(503).json({ error: 'Google authentication is not configured' });
    return;
  }
  const parsed = z.enum(['native', 'web']).safeParse(request.query.flow);
  if (!parsed.success) { response.status(400).json({ error: 'Invalid Google authentication flow' }); return; }
  await pool.query('DELETE FROM google_auth_attempts WHERE expires_at<=NOW()');
  const state = randomBytes(32).toString('base64url');
  const client = oauthClient();
  const verifier = await client.generateCodeVerifierAsync();
  const returnUrl = parsed.data === 'native' ? config.google.appReturnUrl : config.google.webReturnUrl;
  await pool.query(
    `INSERT INTO google_auth_attempts (state_hash,code_verifier,return_url,expires_at)
     VALUES ($1,$2,$3,NOW()+INTERVAL '10 minutes')`,
    [digest(state), verifier.codeVerifier, returnUrl],
  );
  const authorizationUrl = client.generateAuthUrl({
    access_type: 'online',
    scope: ['openid', 'email', 'profile'],
    prompt: 'select_account',
    state,
    code_challenge: verifier.codeChallenge,
    code_challenge_method: CodeChallengeMethod.S256,
  });
  response.setHeader('Cache-Control', 'no-store');
  response.redirect(302, authorizationUrl);
});

googleAuthRouter.get('/auth/google/callback', async (request, response) => {
  const state = typeof request.query.state === 'string' ? request.query.state : '';
  if (!state) { response.status(400).send('Missing Google authentication state.'); return; }
  const attempt = await pool.query<{ code_verifier: string; return_url: string }>(
    `DELETE FROM google_auth_attempts WHERE state_hash=$1 AND expires_at>NOW()
     RETURNING code_verifier,return_url`,
    [digest(state)],
  );
  if (!attempt.rows[0]) { response.status(400).send('Google authentication request expired.'); return; }
  const returnUrl = attempt.rows[0].return_url;
  if (request.query.error) {
    response.redirect(302, callbackUrl(returnUrl, 'google_error', 'cancelled'));
    return;
  }
  const code = typeof request.query.code === 'string' ? request.query.code : '';
  if (!code) { response.redirect(302, callbackUrl(returnUrl, 'google_error', 'invalid_response')); return; }
  try {
    const client = oauthClient();
    const tokenResponse = await client.getToken({ code, codeVerifier: attempt.rows[0].code_verifier });
    if (!tokenResponse.tokens.id_token || !config.google.clientId) throw new Error('Google ID token is missing');
    const verified = await client.verifyIdToken({
      idToken: tokenResponse.tokens.id_token,
      audience: config.google.clientId,
    });
    const payload = verified.getPayload();
    if (!payload?.sub || !payload.email || payload.email_verified !== true) {
      throw new Error('Google email is missing or unverified');
    }
    const user = await findOrCreateGoogleUser(
      payload.sub,
      payload.email.trim().toLowerCase(),
      payload.name ?? '',
    );
    const ticket = randomBytes(32).toString('base64url');
    await pool.query('DELETE FROM google_auth_tickets WHERE expires_at<=NOW()');
    await pool.query(
      `INSERT INTO google_auth_tickets (ticket_hash,user_id,expires_at)
       VALUES ($1,$2,NOW()+INTERVAL '2 minutes')`,
      [digest(ticket), user.id],
    );
    response.setHeader('Cache-Control', 'no-store');
    response.redirect(302, callbackUrl(returnUrl, 'google_code', ticket));
  } catch (error) {
    console.error('Google authentication callback failed', error);
    response.redirect(302, callbackUrl(returnUrl, 'google_error', 'verification_failed'));
  }
});

googleAuthRouter.post('/auth/google/exchange', exchangeLimit, async (request, response) => {
  const parsed = z.object({ code: z.string().min(32).max(256) }).safeParse(request.body);
  if (!parsed.success) { response.status(400).json({ error: 'Invalid Google authentication code' }); return; }
  const ticket = await pool.query<DatabaseUser>(
    `WITH consumed AS (
       DELETE FROM google_auth_tickets WHERE ticket_hash=$1 AND expires_at>NOW() RETURNING user_id
     ) SELECT users.* FROM users JOIN consumed ON consumed.user_id=users.id
       WHERE users.is_active AND users.deleted_at IS NULL`,
    [digest(parsed.data.code)],
  );
  const user = ticket.rows[0];
  if (!user) { response.status(401).json({ error: 'Google authentication code expired' }); return; }
  const visibleUser = publicUser(user);
  await pool.query(
    "INSERT INTO audit_events (user_id,event_type,details) VALUES ($1,'google_login','{}'::jsonb)",
    [user.id],
  );
  await recordUserAccess(request, user.id);
  response.setHeader('Cache-Control', 'no-store');
  response.json({ token: createToken(visibleUser, user.token_version), user: visibleUser });
});
