import { Readable } from 'node:stream'

import * as Minio from 'minio'

import { runtimeConfig } from '../../config/runtime.js'
import { createHttpError } from '../../utils/http.js'

type ObjectStorageConfig = {
  endpoint: string
  bucket: string
  accessKeyId: string
  secretAccessKey: string
  region: string
}

type StoredObject = {
  reader: Readable
  contentType: string
  size: number
}

export class ObjectStorage {
  private readonly client: Minio.Client
  private readonly bucket: string
  private readonly region: string
  private bucketReady = false

  constructor(config: ObjectStorageConfig) {
    const endpoint = normalizeStorageEndpoint(config.endpoint)
    this.bucket = String(config.bucket || '').trim()
    this.region = String(config.region || 'us-east-1').trim() || 'us-east-1'
    this.client = new Minio.Client({
      endPoint: endpoint.endPoint,
      port: endpoint.port,
      useSSL: endpoint.useSSL,
      accessKey: String(config.accessKeyId || '').trim(),
      secretKey: String(config.secretAccessKey || '').trim(),
    })
  }

  async putObject(input: {
    key: string
    content: Buffer
    contentType: string
    metadata?: Record<string, string>
  }): Promise<void> {
    await this.ensureBucketReady()
    await this.client.putObject(this.bucket, input.key, input.content, input.content.length, {
      'Content-Type': input.contentType,
      ...sanitizeObjectMetadata(input.metadata),
    })
  }

  async getObject(key: string): Promise<StoredObject> {
    await this.ensureBucketReady()
    const objectKey = normalizeObjectKey(key)
    const stat = await this.client.statObject(this.bucket, objectKey)
    const reader = await this.client.getObject(this.bucket, objectKey)
    const metadata = stat.metaData || {}
    const contentType = String(
      metadata['content-type'] ||
        metadata['Content-Type'] ||
        metadata.contentType ||
        'application/octet-stream',
    )

    return {
      reader,
      contentType,
      size: Number(stat.size || 0),
    }
  }

  private async ensureBucketReady(): Promise<void> {
    if (this.bucketReady) {
      return
    }

    const exists = await this.client.bucketExists(this.bucket)
    if (!exists) {
      await this.client.makeBucket(this.bucket, this.region)
    }
    this.bucketReady = true
  }
}

let storageInstance: ObjectStorage | null = null

export function getObjectStorage(): ObjectStorage {
  if (!storageInstance) {
    storageInstance = new ObjectStorage({
      endpoint: runtimeConfig.storage.endpoint,
      bucket: runtimeConfig.storage.bucket,
      accessKeyId: runtimeConfig.storage.accessKeyId,
      secretAccessKey: runtimeConfig.storage.secretAccessKey,
      region: runtimeConfig.storage.region,
    })
  }

  return storageInstance
}

export function normalizeStorageEndpoint(rawEndpoint: unknown) {
  const endpoint = String(rawEndpoint || '').trim()
  const parsed = new URL(/^https?:\/\//i.test(endpoint) ? endpoint : `http://${endpoint}`)
  const port = parsed.port ? Number(parsed.port) : parsed.protocol === 'https:' ? 443 : 80

  return {
    endPoint: parsed.hostname,
    port,
    useSSL: parsed.protocol === 'https:',
  }
}

export function normalizeObjectKey(rawKey: unknown): string {
  const key = String(rawKey || '')
    .trim()
    .replace(/^\/+/, '')
  if (!key || key.includes('..') || key.includes('\\')) {
    throw createHttpError('文件 key 不正确', {
      statusCode: 400,
      errorCode: 'file_key_invalid',
    })
  }
  return key
}

function sanitizeObjectMetadata(metadata: Record<string, string> | undefined) {
  if (!metadata) {
    return {}
  }

  return Object.fromEntries(
    Object.entries(metadata)
      .map(([key, value]) => [String(key || '').trim(), encodeURIComponent(String(value || ''))])
      .filter(([key]) => Boolean(key)),
  )
}
