const fs = require('fs/promises')
const path = require('path')
const crypto = require('crypto')
const sharp = require('sharp')
const { nanoid } = require('nanoid')
const {
  findImageAssetByHash,
  createImageAsset,
  createImageOptimizationJob,
  updateImageOptimizationJobStatus,
} = require('../db')

const UPLOAD_ROOT = path.join(__dirname, '..', '..', 'uploads')
const OPTIMIZED_DIR = 'assets'
const OPTIMIZATION_CONCURRENCY = Math.max(1, Number(process.env.IMAGE_OPTIMIZATION_CONCURRENCY || 1))

let activeCount = 0
const pendingJobs = []

function ensureImageMimeType(file) {
  return typeof file?.mimetype === 'string' && file.mimetype.startsWith('image/')
}

function createMemoryUpload(multer, { fileSize = 6 * 1024 * 1024, maxCount } = {}) {
  return multer({
    storage: multer.memoryStorage(),
    limits: {
      fileSize,
      ...(typeof maxCount === 'number' ? { files: maxCount } : {}),
    },
    fileFilter: (req, file, cb) => {
      if (ensureImageMimeType(file)) return cb(null, true)
      cb(new Error('image_file_required'))
    },
  })
}

function scheduleQueue() {
  while (activeCount < OPTIMIZATION_CONCURRENCY && pendingJobs.length) {
    const job = pendingJobs.shift()
    activeCount += 1
    processQueuedJob(job)
      .then(job.resolve)
      .catch(job.reject)
      .finally(() => {
        activeCount = Math.max(0, activeCount - 1)
        scheduleQueue()
      })
  }
}

async function optimizeAndPersist({ file, width, height, fit, quality }) {
  const { data, info } = await sharp(file.buffer, { failOn: 'none' })
    .rotate()
    .resize({
      width,
      height,
      fit,
      withoutEnlargement: true,
    })
    .webp({ quality })
    .toBuffer({ resolveWithObject: true })

  const contentHash = crypto.createHash('sha256').update(data).digest('hex')
  const existing = await findImageAssetByHash(contentHash)
  if (existing) {
    return {
      src: existing.src,
      size: existing.byteSize,
      originalSize: existing.originalByteSize,
      width: existing.width,
      height: existing.height,
      contentHash: existing.contentHash,
      reused: true,
    }
  }

  const basename = nanoid()
  const shardDirectory = basename.slice(0, 2)
  const filename = basename + '.webp'
  const relativeDir = path.join(OPTIMIZED_DIR, shardDirectory)
  const absoluteDir = path.join(UPLOAD_ROOT, relativeDir)
  const absolutePath = path.join(absoluteDir, filename)
  const src = '/uploads/' + relativeDir.split(path.sep).join('/') + '/' + filename

  await fs.mkdir(absoluteDir, { recursive: true })
  await fs.writeFile(absolutePath, data)

  try {
    const asset = await createImageAsset({
      id: nanoid(),
      contentHash,
      src,
      mimeType: 'image/webp',
      byteSize: data.length,
      originalByteSize: file.size || file.buffer.length,
      width: info.width || 0,
      height: info.height || 0,
    })

    return {
      src: asset.src,
      size: asset.byteSize,
      originalSize: asset.originalByteSize,
      width: asset.width,
      height: asset.height,
      contentHash: asset.contentHash,
      reused: false,
    }
  } catch (error) {
    try {
      await fs.unlink(absolutePath)
    } catch (unlinkError) {
      if (unlinkError?.code !== 'ENOENT') throw unlinkError
    }

    if (error?.code === 'ER_DUP_ENTRY') {
      const asset = await findImageAssetByHash(contentHash)
      if (asset) {
        return {
          src: asset.src,
          size: asset.byteSize,
          originalSize: asset.originalByteSize,
          width: asset.width,
          height: asset.height,
          contentHash: asset.contentHash,
          reused: true,
        }
      }
    }

    throw error
  }
}

async function processQueuedJob(job) {
  await updateImageOptimizationJobStatus({
    id: job.jobId,
    status: 'processing',
    startedAt: Date.now(),
  })

  try {
    const result = await optimizeAndPersist(job)
    await updateImageOptimizationJobStatus({
      id: job.jobId,
      status: 'completed',
      optimizedByteSize: result.size,
      reusedAsset: result.reused,
      finishedAt: Date.now(),
    })
    return result
  } catch (error) {
    await updateImageOptimizationJobStatus({
      id: job.jobId,
      status: 'failed',
      errorMessage: error?.message || 'optimization_failed',
      finishedAt: Date.now(),
    })
    throw error
  }
}

async function writeOptimizedImage({
  file,
  directory,
  width,
  height,
  fit = 'inside',
  quality = 82,
}) {
  if (!file?.buffer?.length) {
    const error = new Error('file_required')
    error.code = 'file_required'
    throw error
  }

  if (!ensureImageMimeType(file)) {
    const error = new Error('image_file_required')
    error.code = 'image_file_required'
    throw error
  }

  const jobId = nanoid()
  await createImageOptimizationJob({
    id: jobId,
    sourceCategory: directory,
    targetDirectory: OPTIMIZED_DIR,
    originalByteSize: file.size || file.buffer.length,
  })

  return new Promise((resolve, reject) => {
    pendingJobs.push({
      jobId,
      file,
      directory,
      width,
      height,
      fit,
      quality,
      resolve: (result) => resolve({ ...result, directory }),
      reject,
    })
    scheduleQueue()
  })
}

function getImageOptimizationQueueState() {
  return {
    concurrency: OPTIMIZATION_CONCURRENCY,
    activeCount,
    pendingCount: pendingJobs.length,
  }
}

module.exports = {
  createMemoryUpload,
  ensureImageMimeType,
  writeOptimizedImage,
  getImageOptimizationQueueState,
}
