import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

import { config } from '../config.js';
import { prisma } from '../prisma.js';

type TelegramInitDataUser = {
  id: number;
  username?: string;
  first_name?: string;
  last_name?: string;
  photo_url?: string;
  language_code?: string;
};

export type TelegramLoginData = {
  id: number;
  first_name?: string;
  last_name?: string;
  username?: string;
  photo_url?: string;
  auth_date: number;
  hash: string;
};

export type TelegramUserPayload = {
  id: number;
  username?: string;
  firstName?: string;
  lastName?: string;
  photoUrl?: string;
  languageCode?: string;
};

type TelegramAuthData = {
  telegramInitData?: string;
  telegramLoginData?: string;
  mapflowSessionToken?: string;
};

function hmacSha256(key: string | Buffer, value: string) {
  return createHmac('sha256', key).update(value).digest();
}

export function sha256(value: string) {
  return createHash('sha256').update(value).digest();
}

export function sha256Hex(value: string) {
  return createHash('sha256').update(value).digest('hex');
}

function assertValidHash(receivedHash: string, expectedHash: Buffer, dataType: string) {
  const received = Buffer.from(receivedHash, 'hex');
  if (received.length !== expectedHash.length) {
    throw new Error(`Telegram ${dataType} hash is invalid.`);
  }

  if (!timingSafeEqual(received, expectedHash)) {
    throw new Error(`Telegram ${dataType} hash is invalid.`);
  }
}

function assertConfiguredBotToken() {
  if (!config.telegramMiniAppBotToken) {
    throw new Error('TELEGRAM_MINI_APP_BOT_TOKEN is required.');
  }
}

function assertFreshAuthDate(authDate: number, dataType: string) {
  if (!Number.isFinite(authDate)) {
    throw new Error(`Telegram ${dataType} auth_date is required.`);
  }

  const ageSeconds = Math.floor(Date.now() / 1000) - authDate;
  if (ageSeconds > config.telegramAuthMaxAgeSeconds) {
    throw new Error(`Telegram ${dataType} is expired.`);
  }
}

function parseTelegramInitData(initData: string): TelegramUserPayload {
  assertConfiguredBotToken();
  const params = new URLSearchParams(initData);
  const receivedHash = params.get('hash');
  if (!receivedHash) {
    throw new Error('Telegram init data hash is required.');
  }

  assertFreshAuthDate(Number(params.get('auth_date')), 'init data');

  const dataCheckString = [...params.entries()]
    .filter(([key]) => key !== 'hash')
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([key, value]) => `${key}=${value}`)
    .join('\n');

  const secretKey = hmacSha256('WebAppData', config.telegramMiniAppBotToken);
  const expectedHash = hmacSha256(secretKey, dataCheckString);
  assertValidHash(receivedHash, expectedHash, 'init data');

  const rawUser = params.get('user');
  if (!rawUser) {
    throw new Error('Telegram user is required.');
  }

  const user = JSON.parse(rawUser) as TelegramInitDataUser;
  return {
    id: user.id,
    username: user.username,
    firstName: user.first_name,
    lastName: user.last_name,
    photoUrl: user.photo_url,
    languageCode: user.language_code,
  };
}

function parseTelegramLoginData(loginData: TelegramLoginData): TelegramUserPayload {
  assertConfiguredBotToken();
  assertFreshAuthDate(Number(loginData.auth_date), 'login data');

  const dataCheckString = Object.entries(loginData)
    .filter(([key, value]) => key !== 'hash' && value !== undefined && value !== null)
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([key, value]) => `${key}=${value}`)
    .join('\n');

  const expectedHash = hmacSha256(sha256(config.telegramMiniAppBotToken), dataCheckString);
  assertValidHash(loginData.hash, expectedHash, 'login data');

  return {
    id: Number(loginData.id),
    username: loginData.username,
    firstName: loginData.first_name,
    lastName: loginData.last_name,
    photoUrl: loginData.photo_url,
  };
}

function parseTelegramLoginJson(loginData: string) {
  return parseTelegramLoginData(JSON.parse(loginData) as TelegramLoginData);
}

export async function upsertTelegramUser(user: TelegramUserPayload) {
  return prisma.user.upsert({
    where: { telegramId: String(user.id) },
    create: {
      telegramId: String(user.id),
      username: user.username,
      firstName: user.firstName,
      lastName: user.lastName,
      photoUrl: user.photoUrl,
      languageCode: user.languageCode,
    },
    update: {
      username: user.username,
      firstName: user.firstName,
      lastName: user.lastName,
      photoUrl: user.photoUrl,
      languageCode: user.languageCode,
    },
  });
}

export async function getOrCreateTelegramUser(initData: string) {
  return upsertTelegramUser(parseTelegramInitData(initData));
}

export async function getOrCreateTelegramLoginUser(loginData: TelegramLoginData) {
  return upsertTelegramUser(parseTelegramLoginData(loginData));
}

export async function getUserBySessionToken(sessionToken: string) {
  const session = await prisma.userSession.findUnique({
    where: { tokenHash: sha256Hex(sessionToken) },
    include: { user: true },
  });

  if (!session || session.expiresAt <= new Date()) {
    throw new Error('Telegram authorization is required.');
  }

  return session.user;
}

export async function requireTelegramUser(authData: TelegramAuthData) {
  if (authData.mapflowSessionToken) {
    return getUserBySessionToken(authData.mapflowSessionToken);
  }

  if (authData.telegramInitData) {
    return upsertTelegramUser(parseTelegramInitData(authData.telegramInitData));
  }

  if (authData.telegramLoginData) {
    return upsertTelegramUser(parseTelegramLoginJson(authData.telegramLoginData));
  }

  throw new Error('Telegram authorization is required.');
}

export async function requireAdminTelegramUser(authData: TelegramAuthData) {
  const user = await requireTelegramUser(authData);
  if (!user.isAdmin) {
    throw new Error('MapFlow admin access is required.');
  }

  return user;
}
