import { and, eq, gt, isNull } from "drizzle-orm";
import { getDb } from "../../db/client.js";
import { users, refreshTokens } from "../../db/schema.js";
import { hashPassword, verifyPassword } from "../../utils/password.js";
import { signAccessToken } from "../../utils/jwt.js";
import { generateRefreshToken, hashToken, newId } from "../../utils/tokens.js";
import { REFRESH_TOKEN_TTL_MS } from "../../config/env.js";
import { HttpError } from "../../middleware/errorHandler.js";

export interface PublicUser {
  id: string;
  email: string;
  username: string | null;
  displayName: string;
  avatarUrl: string | null;
}

export interface AuthResult {
  user: PublicUser;
  accessToken: string;
  refreshToken: string;
}

function toPublicUser(u: typeof users.$inferSelect): PublicUser {
  return {
    id: u.id,
    email: u.email,
    username: u.username,
    displayName: u.displayName,
    avatarUrl: u.avatarUrl,
  };
}

async function issueTokens(
  userId: string,
): Promise<{ accessToken: string; refreshToken: string }> {
  const db = getDb();
  const now = Date.now();
  const { token, hash } = generateRefreshToken();

  await db.insert(refreshTokens).values({
    id: newId(),
    userId,
    tokenHash: hash,
    expiresAt: now + REFRESH_TOKEN_TTL_MS,
    revokedAt: null,
    createdAt: now,
  });

  return { accessToken: signAccessToken(userId), refreshToken: token };
}

export async function register(input: {
  email: string;
  password: string;
  displayName: string;
  username?: string;
  desiredId?: string;
}): Promise<AuthResult> {
  const db = getDb();
  const email = input.email.toLowerCase().trim();

  const existing = await db
    .select({ id: users.id })
    .from(users)
    .where(eq(users.email, email))
    .limit(1);
  if (existing.length > 0) {
    throw new HttpError(409, "email_taken");
  }

  if (input.username) {
    const takenUsername = await db
      .select({ id: users.id })
      .from(users)
      .where(eq(users.username, input.username))
      .limit(1);
    if (takenUsername.length > 0) {
      throw new HttpError(409, "username_taken");
    }
  }

  // Reuse the client's local profile id as the account id (zero-mapping sync).
  let userId = input.desiredId || newId();
  if (input.desiredId) {
    const clash = await db
      .select({ id: users.id })
      .from(users)
      .where(eq(users.id, input.desiredId))
      .limit(1);
    if (clash.length > 0) userId = newId();
  }

  const now = Date.now();
  const passwordHash = await hashPassword(input.password);

  const [user] = await db
    .insert(users)
    .values({
      id: userId,
      email,
      username: input.username || null,
      passwordHash,
      displayName: input.displayName.trim() || email,
      avatarUrl: null,
      createdAt: now,
      updatedAt: now,
    })
    .returning();

  const tokens = await issueTokens(user.id);
  return { user: toPublicUser(user), ...tokens };
}

export async function login(input: {
  email: string;
  password: string;
}): Promise<AuthResult> {
  const db = getDb();
  const email = input.email.toLowerCase().trim();

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

  if (!user || !(await verifyPassword(input.password, user.passwordHash))) {
    throw new HttpError(401, "invalid_credentials");
  }

  const tokens = await issueTokens(user.id);
  return { user: toPublicUser(user), ...tokens };
}

// Validates a refresh token and rotates it (single-use). Returns a fresh
// access token, a new refresh token, and the owning user.
export async function refresh(rawToken: string): Promise<AuthResult> {
  const db = getDb();
  const hash = hashToken(rawToken);
  const now = Date.now();

  const [row] = await db
    .select()
    .from(refreshTokens)
    .where(
      and(
        eq(refreshTokens.tokenHash, hash),
        isNull(refreshTokens.revokedAt),
        gt(refreshTokens.expiresAt, now),
      ),
    )
    .limit(1);

  if (!row) {
    throw new HttpError(401, "invalid_refresh_token");
  }

  // Rotate: revoke the used token before issuing a new one.
  await db
    .update(refreshTokens)
    .set({ revokedAt: now })
    .where(eq(refreshTokens.id, row.id));

  const [user] = await db
    .select()
    .from(users)
    .where(eq(users.id, row.userId))
    .limit(1);
  if (!user) {
    throw new HttpError(401, "invalid_refresh_token");
  }

  const tokens = await issueTokens(user.id);
  return { user: toPublicUser(user), ...tokens };
}

export async function logout(rawToken: string): Promise<void> {
  const db = getDb();
  const hash = hashToken(rawToken);
  await db
    .update(refreshTokens)
    .set({ revokedAt: Date.now() })
    .where(eq(refreshTokens.tokenHash, hash));
}
