import 'server-only'
import { promises as fs } from 'fs'
import path from 'path'
import { randomUUID } from 'crypto'
import sharp from 'sharp'

const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'

// Long edge of the "full" version saved alongside each item. Originals are
// discarded — 1200px is sharp enough on a phone (which rarely shows a photo
// larger than ~1100px wide) and brings the typical inventory photo to well
// under 200KB as WebP.
const FULL_MAX_EDGE = 1200

// Square thumbnail used in lists and identification cards.
const THUMB_SIZE = 400

export interface SavedImage {
  /** Relative path under UPLOAD_DIR, e.g. "2026/05/<uuid>.webp" */
  path: string
  thumbPath: string
  width: number
  height: number
  bytes: number
  takenAt: Date | null
}

function partitionedPath(filename: string) {
  const now = new Date()
  const yyyy = String(now.getFullYear())
  const mm = String(now.getMonth() + 1).padStart(2, '0')
  return path.posix.join(yyyy, mm, filename)
}

async function ensureDir(absPath: string) {
  await fs.mkdir(path.dirname(absPath), { recursive: true })
}

export async function saveItemImage(buffer: Buffer): Promise<SavedImage> {
  const id = randomUUID()
  const fullRel = partitionedPath(`${id}.webp`)
  const thumbRel = partitionedPath(`${id}.thumb.webp`)
  const fullAbs = path.join(UPLOAD_DIR, fullRel)
  const thumbAbs = path.join(UPLOAD_DIR, thumbRel)

  await ensureDir(fullAbs)

  const base = sharp(buffer).rotate() // auto-orient via EXIF, then strip on encode

  const fullInfo = await base
    .clone()
    .resize({
      width: FULL_MAX_EDGE,
      height: FULL_MAX_EDGE,
      fit: 'inside',
      withoutEnlargement: true,
    })
    .webp({ quality: 80 })
    .toFile(fullAbs)

  await base
    .clone()
    .resize({
      width: THUMB_SIZE,
      height: THUMB_SIZE,
      fit: 'cover',
      position: 'attention',
    })
    .webp({ quality: 70 })
    .toFile(thumbAbs)

  return {
    path: fullRel,
    thumbPath: thumbRel,
    width: fullInfo.width,
    height: fullInfo.height,
    bytes: fullInfo.size,
    takenAt: null,
  }
}

// Long edge for uploaded maps. Larger than item photos because users may
// zoom into a drone aerial or floor plan to drop a pin on a small feature.
const MAP_MAX_EDGE = 2400

export interface SavedMapImage {
  path: string
  width: number
  height: number
  bytes: number
}

export async function saveMapImage(buffer: Buffer): Promise<SavedMapImage> {
  const id = randomUUID()
  const rel = partitionedPath(`${id}.map.webp`)
  const abs = path.join(UPLOAD_DIR, rel)
  await ensureDir(abs)

  const info = await sharp(buffer)
    .rotate()
    .resize({
      width: MAP_MAX_EDGE,
      height: MAP_MAX_EDGE,
      fit: 'inside',
      withoutEnlargement: true,
    })
    .webp({ quality: 82 })
    .toFile(abs)

  return {
    path: rel,
    width: info.width,
    height: info.height,
    bytes: info.size,
  }
}

export async function readUpload(relPath: string): Promise<Buffer | null> {
  // Reject any path that tries to escape UPLOAD_DIR. The route handler is
  // the only caller; this is defense in depth in case it ever isn't.
  const safeRel = path.posix.normalize(relPath).replace(/^(\.\.[/\\])+/, '')
  if (safeRel.startsWith('/') || safeRel.includes('..')) return null
  const abs = path.join(UPLOAD_DIR, safeRel)
  try {
    return await fs.readFile(abs)
  } catch {
    return null
  }
}

export async function deleteUpload(relPath: string): Promise<void> {
  const safeRel = path.posix.normalize(relPath).replace(/^(\.\.[/\\])+/, '')
  if (safeRel.startsWith('/') || safeRel.includes('..')) return
  const abs = path.join(UPLOAD_DIR, safeRel)
  try {
    await fs.unlink(abs)
  } catch {
    // ignore — file may have been cleaned up already
  }
}

export function contentTypeFor(relPath: string): string {
  const ext = path.extname(relPath).toLowerCase()
  if (ext === '.webp') return 'image/webp'
  if (ext === '.png') return 'image/png'
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg'
  if (ext === '.gif') return 'image/gif'
  if (ext === '.webm') return 'audio/webm'
  if (ext === '.m4a' || ext === '.mp4') return 'audio/mp4'
  if (ext === '.ogg' || ext === '.opus') return 'audio/ogg'
  if (ext === '.mp3') return 'audio/mpeg'
  return 'application/octet-stream'
}

export interface SavedVoiceNote {
  path: string
  mime: string
  bytes: number
}

// Voice notes are saved as-is. The recorder picks a codec the same browser
// can play back; identify-time transcription happens on the dev Mac via
// whisper.cpp (which handles webm/mp4/ogg via ffmpeg, so codec doesn't matter).
export async function saveVoiceNote(buffer: Buffer, mime: string): Promise<SavedVoiceNote> {
  const id = randomUUID()
  let ext = '.webm'
  if (mime.includes('mp4')) ext = '.m4a'
  else if (mime.includes('ogg')) ext = '.ogg'
  else if (mime.includes('mpeg')) ext = '.mp3'
  const rel = partitionedPath(`${id}${ext}`)
  const abs = path.join(UPLOAD_DIR, rel)
  await ensureDir(abs)
  await fs.writeFile(abs, buffer)
  return { path: rel, mime, bytes: buffer.byteLength }
}
