import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { randomUUID } from "node:crypto";
import { stat, mkdir, unlink, writeFile } from "node:fs/promises";
import { basename, join, resolve } from "node:path";
import { eq } from "drizzle-orm";
import sharp, { type Metadata } from "sharp";
import { getDb, schema } from "@slackwsh/data";

const PROFILE_PHOTO_ROOT = resolve(process.cwd(), process.env.UPLOAD_DIR ?? ".uploads", "profiles");
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
const MIN_DIMENSION = 512;
const MAX_DIMENSION = 1024;
const ALLOWED_FORMATS = new Set(["jpeg", "png", "webp", "gif"]);

function publicUser(user: typeof schema.users.$inferSelect) {
  return {
    id: user.id,
    email: user.email,
    emailVerifiedAt: user.emailVerifiedAt?.toISOString() ?? null,
    name: user.name,
    username: user.username,
    avatarUrl: user.avatarUrl,
    tz: user.tz,
    createdAt: user.createdAt.toISOString(),
  };
}

function storedPhotoName(avatarUrl: string | null): string | null {
  if (!avatarUrl) return null;
  const match = /\/profile-photos\/\d+\/([a-f0-9-]+\.webp)$/i.exec(avatarUrl);
  return match?.[1] ? basename(match[1]) : null;
}

@Injectable()
export class ProfileService {
  async getProfile(userId: number) {
    const user = await this.findUser(userId);
    return publicUser(user);
  }

  async setPhoto(userId: number, buffer: Buffer, declaredType: string) {
    if (buffer.length === 0) throw new BadRequestException("photo is empty");
    if (buffer.length > MAX_UPLOAD_BYTES) throw new BadRequestException("photo must be 10 MB or smaller");
    if (!declaredType.startsWith("image/")) throw new BadRequestException("photo must be an image");

    let metadata: Metadata;
    try {
      metadata = await sharp(buffer, { animated: false, failOn: "warning", limitInputPixels: 25_000_000 }).metadata();
    } catch {
      throw new BadRequestException("photo could not be processed; use JPEG, PNG, WebP, or GIF");
    }

    if (!metadata.format || !ALLOWED_FORMATS.has(metadata.format)) {
      throw new BadRequestException("photo must be JPEG, PNG, WebP, or GIF");
    }
    if (!metadata.width || !metadata.height) throw new BadRequestException("photo dimensions could not be read");
    if (metadata.width < MIN_DIMENSION || metadata.height < MIN_DIMENSION) {
      throw new BadRequestException("photo must be at least 512 by 512 pixels");
    }
    if (metadata.width > MAX_DIMENSION || metadata.height > MAX_DIMENSION) {
      throw new BadRequestException("photo must be no larger than 1024 by 1024 pixels");
    }

    const user = await this.findUser(userId);
    const photoName = `${randomUUID()}.webp`;
    const userDir = join(PROFILE_PHOTO_ROOT, String(userId));
    const photoPath = join(userDir, photoName);
    await mkdir(userDir, { recursive: true });

    let output: Buffer;
    try {
      output = await sharp(buffer, { animated: false, failOn: "warning" })
        .rotate()
        .resize(512, 512, { fit: "cover", position: "centre" })
        .webp({ quality: 88 })
        .toBuffer();
    } catch {
      throw new BadRequestException("photo could not be processed; use JPEG, PNG, WebP, or GIF");
    }

    await writeFile(photoPath, output);
    const avatarUrl = `/profile-photos/${userId}/${photoName}`;
    try {
      const db = getDb();
      const [updated] = await db
        .update(schema.users)
        .set({ avatarUrl })
        .where(eq(schema.users.id, userId))
        .returning();
      if (!updated) throw new NotFoundException("user not found");
      await this.removeStoredPhoto(userId, user.avatarUrl);
      return publicUser(updated);
    } catch (error) {
      await unlink(photoPath).catch(() => undefined);
      throw error;
    }
  }

  async removePhoto(userId: number) {
    const user = await this.findUser(userId);
    const db = getDb();
    const [updated] = await db
      .update(schema.users)
      .set({ avatarUrl: null })
      .where(eq(schema.users.id, userId))
      .returning();
    if (!updated) throw new NotFoundException("user not found");
    await this.removeStoredPhoto(userId, user.avatarUrl);
    return publicUser(updated);
  }

  async photoPath(userId: number, requestedName: string): Promise<string> {
    const photoName = basename(requestedName);
    if (!/^[a-f0-9-]+\.webp$/i.test(photoName)) throw new NotFoundException("photo not found");
    const path = join(PROFILE_PHOTO_ROOT, String(userId), photoName);
    const info = await stat(path).catch(() => null);
    if (!info?.isFile()) throw new NotFoundException("photo not found");
    return path;
  }

  private async findUser(userId: number) {
    const db = getDb();
    const [user] = await db.select().from(schema.users).where(eq(schema.users.id, userId)).limit(1);
    if (!user) throw new NotFoundException("user not found");
    return user;
  }

  private async removeStoredPhoto(userId: number, avatarUrl: string | null) {
    const oldName = storedPhotoName(avatarUrl);
    if (!oldName) return;
    await unlink(join(PROFILE_PHOTO_ROOT, String(userId), oldName)).catch(() => undefined);
  }
}
