import { randomUUID } from 'node:crypto'
import crypto from 'node:crypto'
import { createReadStream, createWriteStream } from 'node:fs'
import { mkdir, rm, stat, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { Readable, Transform } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { fileURLToPath } from 'node:url'
import { requestTooLargeError } from './http.mjs'

const moduleDir = path.dirname(fileURLToPath(import.meta.url))
const backendRoot = path.resolve(moduleDir, '../..')
const bucketRoot = process.env.FILE_BUCKET_DIR || path.join(backendRoot, 'storage/uploads')
const defaultRawUploadMaxBytes = 1024 * 1024 * 1024

export function rawUploadMaxBytes() {
  const configured = Number(process.env.RAW_UPLOAD_MAX_BYTES || defaultRawUploadMaxBytes)
  return Number.isFinite(configured) && configured > 0 ? configured : defaultRawUploadMaxBytes
}

function s3Config() {
  const endpoint = process.env.S3_ENDPOINT || ''
  const accessKeyId = process.env.S3_ACCESS_KEY_ID || process.env.MINIO_ROOT_USER || ''
  const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY || process.env.MINIO_ROOT_PASSWORD || ''
  const region = process.env.S3_REGION || 'local'
  const privateBucket = process.env.S3_BUCKET_PRIVATE || 'dlbdi-private'
  const publicBucket = process.env.S3_BUCKET_PUBLIC || 'dlbdi-public'
  return { endpoint, accessKeyId, secretAccessKey, region, privateBucket, publicBucket }
}

function useS3Storage() {
  const config = s3Config()
  return (process.env.OBJECT_STORAGE_DRIVER || '').toLowerCase() === 's3'
    || Boolean(config.endpoint && config.accessKeyId && config.secretAccessKey)
}

function safeFileName(fileName = 'download') {
  return String(fileName)
    .replaceAll('\\', '/')
    .split('/')
    .pop()
    ?.replace(/[^\w.\-\u4e00-\u9fa5]+/g, '_')
    .replace(/^_+|_+$/g, '') || 'download'
}

function extensionOf(fileName) {
  const ext = path.extname(fileName)
  if (!ext || ext.length > 16) return ''
  return ext
}

function encodeS3PathPart(value) {
  return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)
}

function s3ObjectUrl(config, bucket, objectKey) {
  const endpoint = new URL(config.endpoint)
  const basePath = endpoint.pathname.replace(/\/+$/, '')
  const encodedKey = objectKey.split('/').map(encodeS3PathPart).join('/')
  endpoint.pathname = `${basePath}/${encodeS3PathPart(bucket)}/${encodedKey}`
  endpoint.search = ''
  return endpoint
}

function sha256(value) {
  return crypto.createHash('sha256').update(value).digest('hex')
}

function hmac(key, value, encoding) {
  return crypto.createHmac('sha256', key).update(value).digest(encoding)
}

function amzDates(date = new Date()) {
  const iso = date.toISOString().replace(/[:-]|\.\d{3}/g, '')
  return { amzDate: iso, dateStamp: iso.slice(0, 8) }
}

function signingKey(secretAccessKey, dateStamp, region) {
  const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp)
  const regionKey = hmac(dateKey, region)
  const serviceKey = hmac(regionKey, 's3')
  return hmac(serviceKey, 'aws4_request')
}

function signS3Request({ method, url, body = Buffer.alloc(0), payloadHash = '', headers = {}, config }) {
  const { amzDate, dateStamp } = amzDates()
  const bodyHash = payloadHash || sha256(body)
  const normalizedHeaders = {
    host: url.host,
    'x-amz-content-sha256': bodyHash,
    'x-amz-date': amzDate,
    ...Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), String(value).trim()])),
  }
  const signedHeaderNames = Object.keys(normalizedHeaders).sort()
  const canonicalHeaders = signedHeaderNames.map((key) => `${key}:${normalizedHeaders[key]}\n`).join('')
  const canonicalRequest = [
    method,
    url.pathname,
    url.searchParams.toString(),
    canonicalHeaders,
    signedHeaderNames.join(';'),
    bodyHash,
  ].join('\n')
  const credentialScope = `${dateStamp}/${config.region}/s3/aws4_request`
  const stringToSign = [
    'AWS4-HMAC-SHA256',
    amzDate,
    credentialScope,
    sha256(canonicalRequest),
  ].join('\n')
  const signature = hmac(signingKey(config.secretAccessKey, dateStamp, config.region), stringToSign, 'hex')
  return {
    ...normalizedHeaders,
    authorization: `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames.join(';')}, Signature=${signature}`,
  }
}

async function putS3Object({ bucket, objectKey, body, mimeType }) {
  const config = s3Config()
  if (!config.endpoint || !config.accessKeyId || !config.secretAccessKey) {
    const error = new Error('S3 storage is not configured')
    error.code = 's3_not_configured'
    throw error
  }
  const url = s3ObjectUrl(config, bucket, objectKey)
  const headers = signS3Request({
    method: 'PUT',
    url,
    body,
    headers: {
      'content-type': mimeType || 'application/octet-stream',
    },
    config,
  })
  const response = await fetch(url, { method: 'PUT', headers, body })
  if (!response.ok) {
    const detail = await response.text().catch(() => '')
    const error = new Error(`S3 put object failed: ${response.status} ${detail}`)
    error.statusCode = 502
    error.code = 's3_put_failed'
    throw error
  }
}

async function putS3ObjectFromFile({ bucket, objectKey, filePath, fileSize, mimeType, payloadHash }) {
  const config = s3Config()
  if (!config.endpoint || !config.accessKeyId || !config.secretAccessKey) {
    const error = new Error('S3 storage is not configured')
    error.code = 's3_not_configured'
    throw error
  }
  const url = s3ObjectUrl(config, bucket, objectKey)
  const headers = signS3Request({
    method: 'PUT',
    url,
    payloadHash,
    headers: {
      'content-length': fileSize,
      'content-type': mimeType || 'application/octet-stream',
    },
    config,
  })
  const response = await fetch(url, {
    method: 'PUT',
    headers,
    body: createReadStream(filePath),
    duplex: 'half',
  })
  if (!response.ok) {
    const detail = await response.text().catch(() => '')
    const error = new Error(`S3 put object failed: ${response.status} ${detail}`)
    error.statusCode = 502
    error.code = 's3_put_failed'
    throw error
  }
}

async function getS3ObjectStream(file) {
  const config = s3Config()
  const bucket = file.bucket || file.storageBucket || config.privateBucket
  const objectKey = file.objectKey || String(file.storagePath || '').replace(new RegExp(`^${bucket}/`), '')
  const url = s3ObjectUrl(config, bucket, objectKey)
  const headers = signS3Request({ method: 'GET', url, config })
  const response = await fetch(url, { method: 'GET', headers })
  if (!response.ok || !response.body) {
    const detail = await response.text().catch(() => '')
    const error = new Error(`S3 get object failed: ${response.status} ${detail}`)
    error.statusCode = response.status
    error.code = 's3_get_failed'
    throw error
  }
  return Readable.fromWeb(response.body)
}

async function headS3Object(file) {
  const config = s3Config()
  const bucket = file.bucket || file.storageBucket || config.privateBucket
  const objectKey = file.objectKey || String(file.storagePath || '').replace(new RegExp(`^${bucket}/`), '')
  const url = s3ObjectUrl(config, bucket, objectKey)
  const headers = signS3Request({ method: 'HEAD', url, config })
  const response = await fetch(url, { method: 'HEAD', headers })
  if (!response.ok) {
    const error = new Error(`S3 head object failed: ${response.status}`)
    error.statusCode = response.status
    error.code = response.status === 404 ? 's3_object_missing' : 's3_head_failed'
    throw error
  }
  return true
}

async function deleteS3Object({ bucket, objectKey }) {
  const config = s3Config()
  if (!config.endpoint || !config.accessKeyId || !config.secretAccessKey) {
    const error = new Error('S3 storage is not configured')
    error.code = 's3_not_configured'
    throw error
  }
  const url = s3ObjectUrl(config, bucket, objectKey)
  const headers = signS3Request({ method: 'DELETE', url, config })
  const response = await fetch(url, { method: 'DELETE', headers })
  if (!response.ok && response.status !== 404) {
    const detail = await response.text().catch(() => '')
    const error = new Error(`S3 delete object failed: ${response.status} ${detail}`)
    error.statusCode = 502
    error.code = 's3_delete_failed'
    throw error
  }
}

async function streamToBuffer(stream) {
  const chunks = []
  for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
  return Buffer.concat(chunks)
}

function assertContentLengthWithinLimit(request, limit) {
  const contentLength = Number(request.headers?.['content-length'] || 0)
  if (Number.isFinite(contentLength) && contentLength > limit) throw requestTooLargeError(limit)
}

function createCountingTransform(limit, onChunk = () => {}) {
  const state = { total: 0 }
  const stream = new Transform({
    transform(chunk, encoding, callback) {
      state.total += chunk.length
      if (state.total > limit) {
        callback(requestTooLargeError(limit))
        return
      }
      try {
        onChunk(chunk)
      } catch (error) {
        callback(error)
        return
      }
      callback(null, chunk)
    },
  })
  return { stream, state }
}

export async function probeObjectStorageHealth() {
  const startedAt = Date.now()
  const config = s3Config()
  const configured = Boolean(config.endpoint && config.accessKeyId && config.secretAccessKey && config.privateBucket)
  const driver = (process.env.OBJECT_STORAGE_DRIVER || '').toLowerCase()
  const wantsS3 = driver === 's3'
  const requiresS3 = wantsS3 || process.env.NODE_ENV === 'production'

  if (!configured && !wantsS3) {
    return {
      status: requiresS3 ? 'error' : 'ok',
      configured: false,
      driver: 'local',
      required: requiresS3,
      legacyLocalUploads: Boolean(process.env.FILE_BUCKET_DIR),
      message: requiresS3
        ? 'S3 object storage is required but not configured.'
        : 'Local upload storage is active; configure S3 before production.',
    }
  }

  const bucket = config.privateBucket
  const objectKey = `health/probe-${randomUUID()}.txt`
  const body = Buffer.from(`dlbdi-object-storage-health:${objectKey}`)

  try {
    await putS3Object({
      bucket,
      objectKey,
      body,
      mimeType: 'text/plain; charset=utf-8',
    })
    const stream = await getS3ObjectStream({ bucket, objectKey })
    const downloaded = await streamToBuffer(stream)
    if (!downloaded.equals(body)) {
      const error = new Error('S3 probe readback mismatch')
      error.code = 's3_probe_mismatch'
      throw error
    }
    await deleteS3Object({ bucket, objectKey })
    return {
      status: 'ok',
      configured: true,
      driver: 's3',
      bucket,
      latencyMs: Date.now() - startedAt,
      legacyLocalUploads: Boolean(process.env.FILE_BUCKET_DIR),
    }
  } catch (error) {
    return {
      status: 'error',
      configured,
      driver: 's3',
      bucket,
      code: error?.code || 's3_probe_failed',
      message: error?.message || 'S3 object storage probe failed',
      latencyMs: Date.now() - startedAt,
      legacyLocalUploads: Boolean(process.env.FILE_BUCKET_DIR),
    }
  }
}

export async function saveBufferToObjectStorage(buffer, fileName = 'download', mimeType = 'application/octet-stream') {
  const cleanName = safeFileName(fileName)
  const objectId = randomUUID()
  const now = new Date()
  const prefix = [
    'uploads',
    String(now.getUTCFullYear()),
    String(now.getUTCMonth() + 1).padStart(2, '0'),
    String(now.getUTCDate()).padStart(2, '0'),
  ].join('/')
  const objectKey = `${prefix}/${objectId}${extensionOf(cleanName)}`
  const config = s3Config()
  const bucket = config.privateBucket
  await putS3Object({
    bucket,
    objectKey,
    body: buffer,
    mimeType,
  })
  return {
    id: objectId,
    fileName: cleanName,
    fileSize: buffer.length,
    bucket,
    objectKey,
    storagePath: `${bucket}/${objectKey}`,
    storageDriver: 's3',
  }
}

export async function saveBufferToFileStorage(buffer, fileName = 'download', mimeType = 'application/octet-stream') {
  const body = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer || '')
  const maxBytes = rawUploadMaxBytes()
  if (body.length > maxBytes) throw requestTooLargeError(maxBytes)
  const cleanName = safeFileName(fileName)
  const objectId = randomUUID()
  const now = new Date()
  const prefix = [
    'uploads',
    String(now.getUTCFullYear()),
    String(now.getUTCMonth() + 1).padStart(2, '0'),
    String(now.getUTCDate()).padStart(2, '0'),
  ].join('/')
  const objectKey = `${prefix}/${objectId}${extensionOf(cleanName)}`
  const checksum = sha256(body)

  if (useS3Storage()) {
    const config = s3Config()
    const bucket = config.privateBucket
    await putS3Object({
      bucket,
      objectKey,
      body,
      mimeType,
    })
    return {
      id: objectId,
      fileName: cleanName,
      fileSize: body.length,
      bucket,
      objectKey,
      storagePath: `${bucket}/${objectKey}`,
      storageDriver: 's3',
      checksum,
    }
  }

  await mkdir(path.join(bucketRoot, prefix), { recursive: true })
  const storagePath = path.join(bucketRoot, objectKey)
  await writeFile(storagePath, body, { flag: 'wx' })
  return {
    id: objectId,
    fileName: cleanName,
    fileSize: body.length,
    objectKey,
    storagePath,
    storageDriver: 'local',
    checksum,
  }
}

export async function saveRequestBodyToLocalBucket(request, fileName = 'download', options = {}) {
  const maxBytes = Number(options.maxBytes || rawUploadMaxBytes())
  assertContentLengthWithinLimit(request, maxBytes)

  if (useS3Storage()) {
    const cleanName = safeFileName(fileName)
    const objectId = randomUUID()
    const now = new Date()
    const prefix = [
      'uploads',
      String(now.getUTCFullYear()),
      String(now.getUTCMonth() + 1).padStart(2, '0'),
      String(now.getUTCDate()).padStart(2, '0'),
    ].join('/')
    const objectKey = `${prefix}/${objectId}${extensionOf(cleanName)}`
    const config = s3Config()
    const bucket = config.privateBucket
    const tempDir = path.join(bucketRoot, '.tmp')
    const tempPath = path.join(tempDir, `${objectId}.upload`)
    const hash = crypto.createHash('sha256')
    const { stream, state } = createCountingTransform(maxBytes, (chunk) => hash.update(chunk))

    await mkdir(tempDir, { recursive: true })
    try {
      await pipeline(request, stream, createWriteStream(tempPath, { flags: 'wx' }))
      const checksum = hash.digest('hex')
      await putS3ObjectFromFile({
        bucket,
        objectKey,
        filePath: tempPath,
        fileSize: state.total,
        mimeType: request.headers['content-type'] || 'application/octet-stream',
        payloadHash: checksum,
      })
      return {
        id: objectId,
        fileName: cleanName,
        fileSize: state.total,
        bucket,
        objectKey,
        storagePath: `${bucket}/${objectKey}`,
        storageDriver: 's3',
        checksum,
      }
    } finally {
      await rm(tempPath, { force: true }).catch(() => {})
    }
  }

  await mkdir(bucketRoot, { recursive: true })
  const cleanName = safeFileName(fileName)
  const objectId = randomUUID()
  const objectKey = `${objectId}${extensionOf(cleanName)}`
  const storagePath = path.join(bucketRoot, objectKey)
  const { stream } = createCountingTransform(maxBytes)

  try {
    await pipeline(request, stream, createWriteStream(storagePath, { flags: 'wx' }))
  } catch (error) {
    await rm(storagePath, { force: true }).catch(() => {})
    throw error
  }
  const fileStat = await stat(storagePath)

  return {
    id: objectId,
    fileName: cleanName,
    fileSize: fileStat.size,
    objectKey,
    storagePath,
    storageDriver: 'local',
  }
}

export function createLocalFileReadStream(storagePath) {
  const resolvedPath = path.isAbsolute(storagePath)
    ? storagePath
    : path.join(bucketRoot, storagePath)
  return createReadStream(resolvedPath)
}

export async function createStoredFileReadStream(file) {
  if (file?.storageDriver === 's3') return getS3ObjectStream(file)
  return createLocalFileReadStream(file?.storagePath || file?.objectKey || '')
}

export async function checkStoredFileExists(file) {
  if (!file) return false
  if (file.storageDriver === 'database') return Boolean(file.contentBase64)
  if (file.storageDriver === 's3') return headS3Object(file)
  const target = file.storagePath || file.objectKey || ''
  if (!target) return false
  const resolvedPath = path.isAbsolute(target) ? target : path.join(bucketRoot, target)
  await stat(resolvedPath)
  return true
}
