import type { DocumentSnapshot, IncludedFileValue } from './types'

export const MAX_INCLUDED_FILE_BYTES = 128 * 1024 * 1024

export const includedFileMediaType = (archiveName: string) => {
  const extension = archiveName.slice(archiveName.lastIndexOf('.') + 1).toLowerCase()
  return ({
    bin: 'application/octet-stream',
    csv: 'text/csv',
    jpeg: 'image/jpeg',
    jpg: 'image/jpeg',
    pat: 'text/plain',
    png: 'image/png',
    skf: 'application/octet-stream',
    svg: 'image/svg+xml',
    vrml: 'model/vrml',
    wrl: 'model/vrml',
  } as Record<string, string>)[extension] ?? 'application/octet-stream'
}

export const validateIncludedFileArchiveName = (value: unknown, context = 'Included file') => {
  if (typeof value !== 'string' || value.length === 0 || value.length > 255) throw new TypeError(`${context} archive name must contain between 1 and 255 characters.`)
  if (value === '.' || value === '..' || value.includes('/') || value.includes('\\') || /[\0-\x1f\x7f]/.test(value)) throw new RangeError(`${context} archive name must be a safe file basename.`)
  return value
}

export const validateIncludedFileValue = (value: unknown, context = 'Included file'): IncludedFileValue => {
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${context} requires a structured project resource value.`)
  const record = value as Record<string, unknown>
  if (record.schemaVersion !== 1) throw new TypeError(`${context} schemaVersion must be 1.`)
  const archiveName = validateIncludedFileArchiveName(record.archiveName, context)
  if (typeof record.resourceHash !== 'string' || !/^[a-f0-9]{64}$/i.test(record.resourceHash)) throw new TypeError(`${context} resourceHash must be a SHA-256 hex digest.`)
  if (typeof record.byteLength !== 'number' || !Number.isSafeInteger(record.byteLength) || record.byteLength < 0 || record.byteLength > MAX_INCLUDED_FILE_BYTES) throw new RangeError(`${context} byteLength must be between 0 and ${MAX_INCLUDED_FILE_BYTES}.`)
  if (typeof record.mediaType !== 'string' || record.mediaType.length === 0 || record.mediaType.length > 255 || !/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(record.mediaType)) throw new TypeError(`${context} mediaType must be a simple MIME type.`)
  return { schemaVersion: 1, archiveName, resourceHash: record.resourceHash.toLowerCase(), byteLength: record.byteLength, mediaType: record.mediaType.toLowerCase() }
}

export const uniqueIncludedFileArchiveName = (document: DocumentSnapshot, requestedName: string, exclude?: { objectId: string; propertyName: string }) => {
  const name = validateIncludedFileArchiveName(requestedName)
  const used = new Set(document.objects.flatMap((object) => object.properties.flatMap((property) => {
    if (property.type !== 'App::PropertyFileIncluded' || property.value === null || (exclude?.objectId === object.id && exclude.propertyName === property.name)) return []
    return [validateIncludedFileValue(property.value, `${object.id}.${property.name}`).archiveName]
  })))
  if (!used.has(name)) return name
  const dot = name.lastIndexOf('.')
  const stem = dot > 0 ? name.slice(0, dot) : name
  const extension = dot > 0 ? name.slice(dot) : ''
  for (let suffix = 1; suffix < 1_000_000; suffix += 1) {
    const candidate = `${stem}${suffix}${extension}`
    if (candidate.length <= 255 && !used.has(candidate)) return candidate
  }
  throw new Error(`Unable to allocate a unique included-file archive name for ${name}.`)
}
