import express, { Request, Response } from 'express';
import crypto from 'crypto';
import fetch from 'node-fetch';
import jwt from 'jsonwebtoken';
import { db } from '../db/database';
import { JWT_SECRET } from '../config';
import { User } from '../types';

interface OidcDiscoveryDoc {
  authorization_endpoint: string;
  token_endpoint: string;
  userinfo_endpoint: string;
  _issuer?: string;
}

interface OidcTokenResponse {
  access_token?: string;
  id_token?: string;
  token_type?: string;
}

interface OidcUserInfo {
  sub: string;
  email?: string;
  name?: string;
  preferred_username?: string;
  groups?: string[];
  roles?: string[];
  [key: string]: unknown;
}

const router = express.Router();

const AUTH_CODE_TTL = 60000;          // 1 minute
const AUTH_CODE_CLEANUP = 30000;      // 30 seconds
const STATE_TTL = 5 * 60 * 1000;     // 5 minutes
const STATE_CLEANUP = 60 * 1000;      // 1 minute

const authCodes = new Map<string, { token: string; created: number }>();
setInterval(() => {
  const now = Date.now();
  for (const [code, entry] of authCodes) {
    if (now - entry.created > AUTH_CODE_TTL) authCodes.delete(code);
  }
}, AUTH_CODE_CLEANUP);

const pendingStates = new Map<string, { createdAt: number; redirectUri: string; inviteToken?: string }>();

setInterval(() => {
  const now = Date.now();
  for (const [state, data] of pendingStates) {
    if (now - data.createdAt > STATE_TTL) pendingStates.delete(state);
  }
}, STATE_CLEANUP);

function getOidcConfig() {
  const get = (key: string) => (db.prepare("SELECT value FROM app_settings WHERE key = ?").get(key) as { value: string } | undefined)?.value || null;
  const issuer = process.env.OIDC_ISSUER || get('oidc_issuer');
  const clientId = process.env.OIDC_CLIENT_ID || get('oidc_client_id');
  const clientSecret = process.env.OIDC_CLIENT_SECRET || get('oidc_client_secret');
  const displayName = process.env.OIDC_DISPLAY_NAME || get('oidc_display_name') || 'SSO';
  if (!issuer || !clientId || !clientSecret) return null;
  return { issuer: issuer.replace(/\/+$/, ''), clientId, clientSecret, displayName };
}

let discoveryCache: OidcDiscoveryDoc | null = null;
let discoveryCacheTime = 0;
const DISCOVERY_TTL = 60 * 60 * 1000; // 1 hour

async function discover(issuer: string) {
  if (discoveryCache && Date.now() - discoveryCacheTime < DISCOVERY_TTL && discoveryCache._issuer === issuer) {
    return discoveryCache;
  }
  const res = await fetch(`${issuer}/.well-known/openid-configuration`);
  if (!res.ok) throw new Error('Failed to fetch OIDC discovery document');
  const doc = await res.json() as OidcDiscoveryDoc;
  doc._issuer = issuer;
  discoveryCache = doc;
  discoveryCacheTime = Date.now();
  return doc;
}

function generateToken(user: { id: number; username: string; email: string; role: string }) {
  return jwt.sign(
    { id: user.id, username: user.username, email: user.email, role: user.role },
    JWT_SECRET,
    { expiresIn: '24h' }
  );
}

// Check if user should be admin based on OIDC claims
// Env: OIDC_ADMIN_CLAIM (default: "groups"), OIDC_ADMIN_VALUE (required, e.g. "app-trek-admins")
function resolveOidcRole(userInfo: OidcUserInfo, isFirstUser: boolean): 'admin' | 'user' {
  if (isFirstUser) return 'admin';
  const adminValue = process.env.OIDC_ADMIN_VALUE;
  if (!adminValue) return 'user'; // No claim mapping configured
  const claimKey = process.env.OIDC_ADMIN_CLAIM || 'groups';
  const claimData = userInfo[claimKey];
  if (Array.isArray(claimData)) {
    return claimData.some(v => String(v) === adminValue) ? 'admin' : 'user';
  }
  if (typeof claimData === 'string') {
    return claimData === adminValue ? 'admin' : 'user';
  }
  return 'user';
}

function frontendUrl(path: string): string {
  const base = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:5173';
  return base + path;
}

router.get('/login', async (req: Request, res: Response) => {
  const config = getOidcConfig();
  if (!config) return res.status(400).json({ error: 'OIDC not configured' });

  if (config.issuer && !config.issuer.startsWith('https://') && process.env.NODE_ENV === 'production') {
    return res.status(400).json({ error: 'OIDC issuer must use HTTPS in production' });
  }

  try {
    const doc = await discover(config.issuer);
    const state = crypto.randomBytes(32).toString('hex');
    const proto = (req.headers['x-forwarded-proto'] as string) || req.protocol;
    const host = (req.headers['x-forwarded-host'] as string) || req.headers.host;
    const redirectUri = `${proto}://${host}/api/auth/oidc/callback`;
    const inviteToken = req.query.invite as string | undefined;

    pendingStates.set(state, { createdAt: Date.now(), redirectUri, inviteToken });

    const params = new URLSearchParams({
      response_type: 'code',
      client_id: config.clientId,
      redirect_uri: redirectUri,
      scope: 'openid email profile',
      state,
    });

    res.redirect(`${doc.authorization_endpoint}?${params}`);
  } catch (err: unknown) {
    console.error('[OIDC] Login error:', err instanceof Error ? err.message : err);
    res.status(500).json({ error: 'OIDC login failed' });
  }
});

router.get('/callback', async (req: Request, res: Response) => {
  const { code, state, error: oidcError } = req.query as { code?: string; state?: string; error?: string };

  if (oidcError) {
    console.error('[OIDC] Provider error:', oidcError);
    return res.redirect(frontendUrl('/login?oidc_error=' + encodeURIComponent(oidcError)));
  }

  if (!code || !state) {
    return res.redirect(frontendUrl('/login?oidc_error=missing_params'));
  }

  const pending = pendingStates.get(state);
  if (!pending) {
    return res.redirect(frontendUrl('/login?oidc_error=invalid_state'));
  }
  pendingStates.delete(state);

  const config = getOidcConfig();
  if (!config) return res.redirect(frontendUrl('/login?oidc_error=not_configured'));

  if (config.issuer && !config.issuer.startsWith('https://') && process.env.NODE_ENV === 'production') {
    return res.redirect(frontendUrl('/login?oidc_error=issuer_not_https'));
  }

  try {
    const doc = await discover(config.issuer);

    const tokenRes = await fetch(doc.token_endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        redirect_uri: pending.redirectUri,
        client_id: config.clientId,
        client_secret: config.clientSecret,
      }),
    });

    const tokenData = await tokenRes.json() as OidcTokenResponse;
    if (!tokenRes.ok || !tokenData.access_token) {
      console.error('[OIDC] Token exchange failed:', tokenData);
      return res.redirect(frontendUrl('/login?oidc_error=token_failed'));
    }

    const userInfoRes = await fetch(doc.userinfo_endpoint, {
      headers: { Authorization: `Bearer ${tokenData.access_token}` },
    });
    const userInfo = await userInfoRes.json() as OidcUserInfo;

    if (!userInfo.email) {
      return res.redirect(frontendUrl('/login?oidc_error=no_email'));
    }

    const email = userInfo.email.toLowerCase();
    const name = userInfo.name || userInfo.preferred_username || email.split('@')[0];
    const sub = userInfo.sub;

    let user = db.prepare('SELECT * FROM users WHERE oidc_sub = ? AND oidc_issuer = ?').get(sub, config.issuer) as User | undefined;
    if (!user) {
      user = db.prepare('SELECT * FROM users WHERE LOWER(email) = ?').get(email) as User | undefined;
    }

    if (user) {
      if (!user.oidc_sub) {
        db.prepare('UPDATE users SET oidc_sub = ?, oidc_issuer = ? WHERE id = ?').run(sub, config.issuer, user.id);
      }
      // Update role based on OIDC claims on every login (if claim mapping is configured)
      if (process.env.OIDC_ADMIN_VALUE) {
        const newRole = resolveOidcRole(userInfo, false);
        if (user.role !== newRole) {
          db.prepare('UPDATE users SET role = ? WHERE id = ?').run(newRole, user.id);
          user = { ...user, role: newRole } as User;
        }
      }
    } else {
      const userCount = (db.prepare('SELECT COUNT(*) as count FROM users').get() as { count: number }).count;
      const isFirstUser = userCount === 0;

      let validInvite: any = null;
      if (pending.inviteToken) {
        validInvite = db.prepare('SELECT * FROM invite_tokens WHERE token = ?').get(pending.inviteToken);
        if (validInvite) {
          if (validInvite.max_uses > 0 && validInvite.used_count >= validInvite.max_uses) validInvite = null;
          if (validInvite?.expires_at && new Date(validInvite.expires_at) < new Date()) validInvite = null;
        }
      }

      if (!isFirstUser && !validInvite) {
        const setting = db.prepare("SELECT value FROM app_settings WHERE key = 'allow_registration'").get() as { value: string } | undefined;
        if (setting?.value === 'false') {
          return res.redirect(frontendUrl('/login?oidc_error=registration_disabled'));
        }
      }

      const role = resolveOidcRole(userInfo, isFirstUser);
      const randomPass = crypto.randomBytes(32).toString('hex');
      const bcrypt = require('bcryptjs');
      const hash = bcrypt.hashSync(randomPass, 10);

      let username = name.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 30) || 'user';
      const existing = db.prepare('SELECT id FROM users WHERE LOWER(username) = LOWER(?)').get(username);
      if (existing) username = `${username}_${Date.now() % 10000}`;

      const result = db.prepare(
        'INSERT INTO users (username, email, password_hash, role, oidc_sub, oidc_issuer) VALUES (?, ?, ?, ?, ?, ?)'
      ).run(username, email, hash, role, sub, config.issuer);

      if (validInvite) {
        const updated = db.prepare(
          'UPDATE invite_tokens SET used_count = used_count + 1 WHERE id = ? AND (max_uses = 0 OR used_count < max_uses)'
        ).run(validInvite.id);
        if (updated.changes === 0) {
          console.warn(`[OIDC] Invite token ${pending.inviteToken?.slice(0, 8)}... exceeded max_uses (race condition)`);
        }
      }

      user = { id: Number(result.lastInsertRowid), username, email, role } as User;
    }

    db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(user.id);

    const token = generateToken(user);
    const { v4: uuidv4 } = require('uuid');
    const authCode = uuidv4();
    authCodes.set(authCode, { token, created: Date.now() });
    res.redirect(frontendUrl('/login?oidc_code=' + authCode));
  } catch (err: unknown) {
    console.error('[OIDC] Callback error:', err);
    res.redirect(frontendUrl('/login?oidc_error=server_error'));
  }
});

router.get('/exchange', (req: Request, res: Response) => {
  const { code } = req.query as { code?: string };
  if (!code) return res.status(400).json({ error: 'Code required' });
  const entry = authCodes.get(code);
  if (!entry) return res.status(400).json({ error: 'Invalid or expired code' });
  authCodes.delete(code);
  if (Date.now() - entry.created > AUTH_CODE_TTL) return res.status(400).json({ error: 'Code expired' });
  res.json({ token: entry.token });
});

export default router;
