/**
 * Portal file storage helpers — portal-file-sharing (wave-11 leaf-C).
 *
 * R2 key convention: {tenantId}/portal/{customerId}/{uuid}-{sanitizedFilename}
 * Max size: 100 MB. Active-content MIME types blocked; downloads forced attachment.
 *
 * Uses the STORAGE R2 binding (shared bucket, scoped by key prefix).
 * Presigned URLs use the aws4fetch S3-compat pattern (same as kb-storage.ts).
 */
import { AwsClient } from 'aws4fetch'
import type { Env } from '../env'

// ── Constants ─────────────────────────────────────────────────────────────────

export const MAX_PORTAL_FILE_BYTES = 100 * 1024 * 1024  // 100 MB

/** Browser-executable MIME types blocked at upload (stored XSS via presigned GET). */
export const BLOCKED_PORTAL_MIME_TYPES = new Set([
  'text/html',
  'image/svg+xml',
  'application/javascript',
  'text/javascript',
  'application/x-javascript',
  'application/ecmascript',
  'text/ecmascript',
  'application/xhtml+xml',
  'text/xml',
  'application/xml',
])

const PORTAL_KEY_OBJECT_PATTERN =
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-.+$/i

// ── Filename sanitization ─────────────────────────────────────────────────────

/**
 * Strip path separators and control chars; collapse whitespace.
 * UUID prefix ensures key uniqueness — sanitization is for safety/readability.
 */
export function sanitizeFilename(name: string): string {
  return name
    .replace(/[/\\]/g, '_')           // no traversal
    .replace(/[\x00-\x1f\x7f]/g, '') // strip control chars
    .replace(/\s+/g, ' ')            // collapse whitespace
    .trim()
    || 'file'
}

// ── Key builder ───────────────────────────────────────────────────────────────

export function buildPortalFileKey(
  tenantId: string,
  customerId: string,
  filename: string,
): string {
  const safe = sanitizeFilename(filename)
  const uid = crypto.randomUUID()
  return portalFileKeyPrefix(tenantId, customerId) + `${uid}-${safe}`
}

export function portalFileKeyPrefix(tenantId: string, customerId: string): string {
  return `${tenantId}/portal/${customerId}/`
}

// ── Upload validation ─────────────────────────────────────────────────────────

export class PortalFileValidationError extends Error {
  constructor(public readonly code: string, message: string) {
    super(message)
    this.name = 'PortalFileValidationError'
  }
}

export function normalizePortalMimeType(mime: string): string {
  return mime.split(';')[0]!.trim().toLowerCase()
}

export function validateUpload(input: { mime_type: string; file_size_bytes: number }): void {
  if (!input.mime_type || input.mime_type.trim() === '') {
    throw new PortalFileValidationError('INVALID_MIME_TYPE', 'mime_type is required')
  }
  const mime = normalizePortalMimeType(input.mime_type)
  if (BLOCKED_PORTAL_MIME_TYPES.has(mime)) {
    throw new PortalFileValidationError('MIME_NOT_ALLOWED', `File type not permitted: ${mime}`)
  }
  if (input.file_size_bytes > MAX_PORTAL_FILE_BYTES) {
    throw new PortalFileValidationError(
      'FILE_TOO_LARGE',
      `File size ${input.file_size_bytes} exceeds maximum ${MAX_PORTAL_FILE_BYTES} bytes`,
    )
  }
}

/**
 * Ensures r2_key was issued for this tenant+customer upload path.
 * Rejects path traversal and keys outside `${tenantId}/portal/${customerId}/`.
 */
export function validatePortalFileR2Key(
  tenantId: string,
  customerId: string,
  r2Key: string,
): void {
  if (
    !r2Key ||
    r2Key.includes('..') ||
    r2Key.includes('\\') ||
    r2Key.startsWith('/') ||
    r2Key.includes('\0')
  ) {
    throw new PortalFileValidationError('INVALID_R2_KEY', 'Invalid storage key')
  }

  const prefix = portalFileKeyPrefix(tenantId, customerId)
  if (!r2Key.startsWith(prefix)) {
    throw new PortalFileValidationError(
      'INVALID_R2_KEY',
      'Storage key does not match tenant and customer scope',
    )
  }

  const objectPart = r2Key.slice(prefix.length)
  if (!PORTAL_KEY_OBJECT_PATTERN.test(objectPart)) {
    throw new PortalFileValidationError(
      'INVALID_R2_KEY',
      'Storage key does not match expected upload format',
    )
  }
}

function sanitizeContentDispositionFilename(filename: string): string {
  return filename.replace(/[\x00-\x1f\x7f\r\n"]/g, '_').slice(0, 200) || 'download'
}

export function buildAttachmentContentDisposition(filename: string): string {
  const safe = sanitizeContentDispositionFilename(filename)
  return `attachment; filename="${safe}"`
}

// ── R2 S3-compat presign helper ───────────────────────────────────────────────

/**
 * R2Bucket bindings don't expose createPresignedUrl in CF workers-types,
 * but the method exists at runtime. Cast with optional fallback to the
 * S3-compat API via aws4fetch (same pattern as kb-storage.ts).
 */
type R2BucketWithPresign = R2Bucket & {
  createPresignedUrl?: (
    method: string,
    key: string,
    options: { expiresIn: number },
  ) => Promise<string>
}

async function presignR2Url(
  env: Env,
  method: 'GET' | 'PUT',
  r2Key: string,
  expiresIn: number,
  responseOverrides?: { contentDisposition?: string },
  putHeaders?: { contentType?: string; contentLength?: number },
): Promise<string> {
  const storage = env.STORAGE as R2BucketWithPresign
  const needsSignedHeaders = Boolean(
    responseOverrides?.contentDisposition ||
      putHeaders?.contentType ||
      putHeaders?.contentLength !== undefined,
  )

  if (typeof storage.createPresignedUrl === 'function' && !needsSignedHeaders) {
    return storage.createPresignedUrl(method, r2Key, { expiresIn })
  }

  // S3-compat fallback via aws4fetch (also used when signed headers are required)
  const envAny = env as unknown as Record<string, string>
  const accountId = envAny['CF_ACCOUNT_ID'] ?? ''
  const accessKeyId = envAny['R2_ACCESS_KEY_ID'] ?? ''
  const secretAccessKey = envAny['R2_SECRET_ACCESS_KEY'] ?? ''
  const bucketName = envAny['R2_BUCKET_NAME'] ?? 'zync-storage'

  const endpoint = `https://${accountId}.r2.cloudflarestorage.com`
  const aws = new AwsClient({ accessKeyId, secretAccessKey, region: 'auto', service: 's3' })

  const url = new URL(`${endpoint}/${bucketName}/${r2Key}`)
  url.searchParams.set('X-Amz-Expires', String(expiresIn))
  if (responseOverrides?.contentDisposition) {
    url.searchParams.set('response-content-disposition', responseOverrides.contentDisposition)
  }

  const headers = new Headers()
  if (putHeaders?.contentType) {
    headers.set('Content-Type', putHeaders.contentType)
  }
  if (putHeaders?.contentLength !== undefined) {
    headers.set('Content-Length', String(putHeaders.contentLength))
  }

  const presigned = await aws.sign(
    new Request(url.toString(), { method, headers }),
    putHeaders?.contentLength !== undefined
      ? { aws: { signQuery: true, allHeaders: true } }
      : { aws: { signQuery: true } },
  )

  return presigned.url
}

// ── Signed URLs ───────────────────────────────────────────────────────────────

export async function createSignedPutUrl(
  env: Env,
  r2Key: string,
  opts: { contentType: string; expiresIn?: number; contentLength?: number },
): Promise<string> {
  return presignR2Url(env, 'PUT', r2Key, opts.expiresIn ?? 300, undefined, {
    contentType: opts.contentType,
    contentLength: opts.contentLength,
  })
}

export async function createSignedDownloadUrl(
  env: Env,
  r2Key: string,
  opts?: { expiresIn?: number; filename?: string },
): Promise<string> {
  const disposition = buildAttachmentContentDisposition(opts?.filename ?? 'download')
  return presignR2Url(env, 'GET', r2Key, opts?.expiresIn ?? 300, {
    contentDisposition: disposition,
  })
}

// ── Object delete ─────────────────────────────────────────────────────────────

export async function deleteR2Object(env: Env, r2Key: string): Promise<void> {
  await env.STORAGE.delete(r2Key)
}
