import 'server-only'
import fs from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
import sharp from 'sharp'

/**
 * Handwritten math work. The ORIGINAL image is the artifact of record (Rule 6:
 * "the raw student response, verbatim, and the original image, if handwritten"),
 * so the original is written to disk untouched and never overwritten.
 *
 * A downscaled copy is made only to keep the API payload sane. The ORIGINAL is
 * what goes to the grader when it fits; there is no OCR step anywhere in this
 * file, by design — an OCR error is indistinguishable from a wrong answer.
 */

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

/** Anthropic accepts these; anything else is rejected at the form. */
const ALLOWED: Record<string, 'image/jpeg' | 'image/png' | 'image/webp'> = {
  'image/jpeg': 'image/jpeg',
  'image/jpg': 'image/jpeg',
  'image/png': 'image/png',
  'image/webp': 'image/webp',
}

/** Long edge cap for the copy sent to the API. Plenty for reading pencil work. */
const MAX_EDGE = 1568

export function isAllowedImageType(mime: string): boolean {
  return mime.toLowerCase() in ALLOWED
}

/**
 * Writes the original and a sidecar API-sized copy. Returns the ORIGINAL's
 * relative path — that is what goes in the database.
 */
export async function saveUpload(file: File): Promise<string> {
  const mime = ALLOWED[file.type.toLowerCase()]
  if (!mime) throw new Error(`Unsupported image type: ${file.type}`)

  const now = new Date()
  const rel = path.join(
    String(now.getUTCFullYear()),
    String(now.getUTCMonth() + 1).padStart(2, '0'),
  )
  const dir = path.join(UPLOAD_DIR, rel)
  await fs.mkdir(dir, { recursive: true })

  const ext = mime === 'image/jpeg' ? 'jpg' : mime === 'image/png' ? 'png' : 'webp'
  const id = crypto.randomUUID()
  const originalRel = path.join(rel, `${id}.${ext}`)
  const buf = Buffer.from(await file.arrayBuffer())

  // Original, byte-for-byte.
  await fs.writeFile(path.join(UPLOAD_DIR, originalRel), buf)

  // API-sized copy alongside it. Failure here is not fatal — the original is
  // the record, and readUploadAsBase64 falls back to it.
  try {
    await sharp(buf)
      .rotate() // honour EXIF orientation; a sideways photo grades badly
      .resize({ width: MAX_EDGE, height: MAX_EDGE, fit: 'inside', withoutEnlargement: true })
      .webp({ quality: 88 })
      .toFile(path.join(UPLOAD_DIR, rel, `${id}.api.webp`))
  } catch {
    // fall through
  }

  return originalRel
}

export interface LoadedImage {
  data: string
  mediaType: 'image/jpeg' | 'image/png' | 'image/webp'
}

/**
 * Loads an image for the API. Prefers the downscaled sidecar; falls back to the
 * original. Returns null rather than throwing so one unreadable photo does not
 * take down grading for a whole set.
 */
export async function readUploadAsBase64(relPath: string): Promise<LoadedImage | null> {
  const parsed = path.parse(relPath)
  const sidecar = path.join(parsed.dir, `${parsed.name}.api.webp`)

  for (const [candidate, mediaType] of [
    [sidecar, 'image/webp'],
    [relPath, mediaTypeFor(relPath)],
  ] as const) {
    if (!mediaType) continue
    try {
      const buf = await fs.readFile(path.join(UPLOAD_DIR, candidate))
      return { data: buf.toString('base64'), mediaType }
    } catch {
      continue
    }
  }
  return null
}

function mediaTypeFor(p: string): 'image/jpeg' | 'image/png' | 'image/webp' | null {
  const ext = path.extname(p).toLowerCase()
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg'
  if (ext === '.png') return 'image/png'
  if (ext === '.webp') return 'image/webp'
  return null
}

/** Serves an original upload back to the parent's review screen. */
export async function readUploadBytes(
  relPath: string,
): Promise<{ buf: Buffer; mediaType: string } | null> {
  const mediaType = mediaTypeFor(relPath)
  if (!mediaType) return null
  // Contain reads to the upload dir — relPath comes from the DB, but a path
  // traversal there would read arbitrary files off the container.
  const full = path.resolve(UPLOAD_DIR, relPath)
  if (!full.startsWith(path.resolve(UPLOAD_DIR) + path.sep)) return null
  try {
    return { buf: await fs.readFile(full), mediaType }
  } catch {
    return null
  }
}
