import { Router, Request, Response } from 'express';
import { prisma } from '../db/prisma';
import { requireAuth, requires } from '../middleware/auth';
import { isSuperadmin } from '../services/auth';
import { toHexPubkey, normalizePubkey } from '../services/pubkey';
import {
  ROLE_RANK,
  isAssignableRole,
  type EffectiveRole,
} from '../constants/permissions';
import fs from 'fs';
import path from 'path';

const BLOCKED_USERNAMES_PATH = path.resolve(__dirname, '../../config/blocked-usernames.txt');

function getBlockedUsernames(): Set<string> {
  try {
    const content = fs.readFileSync(BLOCKED_USERNAMES_PATH, 'utf-8');
    return new Set(
      content
        .split('\n')
        .map((l) => l.trim().toLowerCase())
        .filter(Boolean)
    );
  } catch {
    return new Set();
  }
}

const USERNAME_REGEX = /^[a-z0-9._-]+$/i;

function validateUsername(
  username: string,
  opts?: { allowReserved?: boolean }
): string | null {
  if (!username || username.trim().length === 0) return 'Username is required';
  if (username.length > 50) return 'Username must be 50 characters or fewer';
  if (!USERNAME_REGEX.test(username)) return 'Username may only contain letters, numbers, dots, hyphens, and underscores';
  if (!opts?.allowReserved) {
    const blocked = getBlockedUsernames();
    if (blocked.has(username.toLowerCase())) return 'This username is reserved';
  }
  return null;
}

function pickHigherRole(a: string | null, b: string | null): string | null {
  const rankOf = (r: string | null): number =>
    r && isAssignableRole(r) ? ROLE_RANK[r as EffectiveRole] : 0;
  return rankOf(a) >= rankOf(b) ? a : b;
}

const router = Router();

router.get(
  '/',
  requireAuth,
  requires('users.assign_role'),
  async (_req: Request, res: Response) => {
    try {
      const users = await prisma.user.findMany({
        orderBy: { createdAt: 'desc' },
      });

      // The same person can have been stored both as hex and as npub (no
      // normalization existed historically). Collapse those into a single
      // identity keyed by hex so the list never shows a duplicate row.
      const byHex = new Map<string, (typeof users)[number]>();
      for (const u of users) {
        const hex = toHexPubkey(u.pubkey) ?? u.pubkey;
        const existing = byHex.get(hex);
        if (!existing) {
          byHex.set(hex, { ...u, pubkey: hex });
          continue;
        }
        // Merge: prefer a stored role over none, the higher-ranked role on
        // conflict, and the first available username. Keep the earliest join.
        const mergedRole = pickHigherRole(existing.role, u.role);
        const mergedUsername = existing.username ?? u.username;
        const earliest = existing.createdAt <= u.createdAt ? existing : u;
        byHex.set(hex, {
          ...existing,
          role: mergedRole,
          username: mergedUsername,
          displayName: existing.displayName ?? u.displayName,
          createdAt: earliest.createdAt,
          pubkey: hex,
        });
      }

      // Flag env-sourced SuperAdmins so the UI can render them locked. Their
      // effective role is reported as superadmin regardless of any stored value.
      const result = [...byHex.values()].map((u) => {
        const superAdmin = isSuperadmin(u.pubkey);
        return {
          ...u,
          role: superAdmin ? 'superadmin' : (u.role ?? null),
          isSuperAdmin: superAdmin,
        };
      });
      res.json(result);
    } catch (err) {
      console.error('List users error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

// Creates a user row from an npub or hex pubkey so admins can pre-register
// people before they have logged in. The pubkey is normalized to hex.
router.post(
  '/',
  requireAuth,
  requires('users.assign_role'),
  async (req: Request, res: Response) => {
    try {
      const { pubkey: rawPubkey } = req.body as { pubkey?: string };
      if (!rawPubkey || typeof rawPubkey !== 'string') {
        res.status(400).json({ error: 'pubkey is required' });
        return;
      }

      const hex = toHexPubkey(rawPubkey);
      if (!hex) {
        res.status(400).json({ error: 'Invalid pubkey. Provide an npub or 64-char hex key.' });
        return;
      }

      const existing = await prisma.user.findUnique({ where: { pubkey: hex } });
      if (existing) {
        res.status(409).json({ error: 'User already exists' });
        return;
      }

      const user = await prisma.user.create({ data: { pubkey: hex } });
      res.status(201).json({ ...user, role: user.role ?? null, isSuperAdmin: isSuperadmin(hex) });
    } catch (err) {
      console.error('Create user error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

// Assigns a role to a user, replacing the old promote/demote toggle. Enforces the
// hierarchy: SuperAdmins are never modifiable here, and a non-SuperAdmin caller
// cannot assign a role at or above their own or modify anyone at or above them.
router.put(
  '/:pubkey/role',
  requireAuth,
  requires('users.assign_role'),
  async (req: Request, res: Response) => {
    try {
      const pubkeyRaw = req.params.pubkey;
      const pubkeyParam =
        typeof pubkeyRaw === 'string' ? pubkeyRaw : Array.isArray(pubkeyRaw) ? pubkeyRaw[0] : '';
      if (!pubkeyParam) {
        res.status(400).json({ error: 'pubkey is required' });
        return;
      }
      const pubkey = normalizePubkey(pubkeyParam);

      const { role } = req.body as { role?: string | null };

      // Normalize the requested role. Null, empty, or "guest" all mean remove
      // any elevated role. SuperAdmin can never be assigned here.
      let newRole: EffectiveRole;
      if (role === null || role === undefined || role === '' || role === 'guest') {
        newRole = 'guest';
      } else if (isAssignableRole(role)) {
        newRole = role;
      } else {
        res.status(400).json({ error: 'Invalid role' });
        return;
      }

      if (isSuperadmin(pubkey)) {
        res.status(403).json({ error: 'SuperAdmins cannot be modified' });
        return;
      }

      const caller = req.access!;
      const callerRank = ROLE_RANK[caller.role];
      const newRank = ROLE_RANK[newRole];

      const target = await prisma.user.findUnique({ where: { pubkey } });
      const targetCurrent: EffectiveRole = isAssignableRole(target?.role)
        ? (target!.role as EffectiveRole)
        : 'guest';
      const targetRank = ROLE_RANK[targetCurrent];

      if (!caller.isSuperAdmin) {
        if (newRank >= callerRank) {
          res.status(403).json({ error: 'You cannot assign a role at or above your own' });
          return;
        }
        if (targetRank >= callerRank) {
          res.status(403).json({ error: 'You cannot modify a user at or above your own role' });
          return;
        }
      }

      const storedRole = newRole === 'guest' ? null : newRole;
      const updated = await prisma.user.upsert({
        where: { pubkey },
        update: { role: storedRole },
        create: { pubkey, role: storedRole },
      });

      res.json({ ...updated, role: updated.role ?? null, isSuperAdmin: false });
    } catch (err) {
      console.error('Assign role error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

router.get(
  '/me/username-check',
  requireAuth,
  async (req: Request, res: Response) => {
    try {
      const username = (req.query.username as string || '').trim().toLowerCase();
      const error = validateUsername(username);
      if (error) {
        res.json({ available: false, reason: error });
        return;
      }

      const existing = await prisma.user.findFirst({
        where: {
          username: { equals: username },
          NOT: { pubkey: req.user!.pubkey },
        },
      });

      if (existing) {
        res.json({ available: false, reason: 'Username is already taken' });
        return;
      }

      res.json({ available: true });
    } catch (err) {
      console.error('Username check error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

router.patch(
  '/me',
  requireAuth,
  async (req: Request, res: Response) => {
    try {
      const { username } = req.body;
      const normalized = (username as string || '').trim().toLowerCase();

      const error = validateUsername(normalized);
      if (error) {
        res.status(400).json({ error });
        return;
      }

      const existing = await prisma.user.findFirst({
        where: {
          username: { equals: normalized },
          NOT: { pubkey: req.user!.pubkey },
        },
      });

      if (existing) {
        res.status(409).json({ error: 'Username is already taken' });
        return;
      }

      const user = await prisma.user.upsert({
        where: { pubkey: req.user!.pubkey },
        update: { username: normalized },
        create: { pubkey: req.user!.pubkey, username: normalized },
      });

      res.json(user);
    } catch (err) {
      console.error('Update profile error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

router.patch(
  '/:pubkey',
  requireAuth,
  requires('nip05.assign'),
  async (req: Request, res: Response) => {
    try {
      const pubkeyRaw = req.params.pubkey;
      const pubkeyParam =
        typeof pubkeyRaw === 'string' ? pubkeyRaw : Array.isArray(pubkeyRaw) ? pubkeyRaw[0] : '';
      if (!pubkeyParam) {
        res.status(400).json({ error: 'pubkey is required' });
        return;
      }
      const hex = normalizePubkey(pubkeyParam);

      const { username } = req.body;
      const normalized = (username as string || '').trim().toLowerCase();

      const error = validateUsername(normalized, { allowReserved: true });
      if (error) {
        res.status(400).json({ error });
        return;
      }

      // Tolerate legacy rows still stored as npub by matching either form.
      const target = await prisma.user.findFirst({
        where: { OR: [{ pubkey: hex }, { pubkey: pubkeyParam }] },
      });
      if (!target) {
        res.status(404).json({ error: 'User not found' });
        return;
      }

      const existing = await prisma.user.findFirst({
        where: {
          username: { equals: normalized },
          NOT: { pubkey: target.pubkey },
        },
      });

      if (existing) {
        res.status(409).json({ error: 'Username is already taken' });
        return;
      }

      const user = await prisma.user.update({
        where: { pubkey: target.pubkey },
        data: { username: normalized },
      });

      res.json(user);
    } catch (err) {
      console.error('Admin update user username error:', err);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
);

export default router;
