import { Readable } from 'node:stream'

import {
  CreateBucketCommand,
  DeleteObjectCommand,
  GetObjectCommand,
  HeadBucketCommand,
  HeadObjectCommand,
  ListObjectsV2Command,
  PutObjectCommand,
  S3Client,
} from '@aws-sdk/client-s3'

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

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

export type StorageObjectSummary = {
  key: string
  size: number
  etag: string
  lastModified?: Date
}

export type StoredObjectStat = {
  key: string
  size: number
  etag: string
  contentType: string
  lastModified?: Date
}

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

export class ObjectStorage {
  private readonly client: S3Client
  private readonly bucket: string
  private readonly pathStyle: boolean
  private bucketReady = false
  private bucketReadyPromise: Promise<void> | null = null

  constructor(config: ObjectStorageConfig) {
    const endpoint = normalizeStorageEndpoint(config.endpoint)
    this.bucket = String(config.bucket || '').trim()
    // 阿里云 OSS 强制虚拟主机风格（bucket.endpoint）；MinIO 等自建端点走 path-style。
    this.pathStyle = !endpoint.endPoint.toLowerCase().endsWith('.aliyuncs.com')
    this.client = new S3Client({
      region: String(config.region || 'us-east-1').trim() || 'us-east-1',
      endpoint: buildEndpointUrl(endpoint),
      forcePathStyle: this.pathStyle,
      credentials: {
        accessKeyId: String(config.accessKeyId || '').trim(),
        secretAccessKey: String(config.secretAccessKey || '').trim(),
      },
      // 默认的 CRC32 请求校验头不被 OSS 的 S3 兼容层支持，关闭以保持兼容。
      requestChecksumCalculation: 'WHEN_REQUIRED',
      responseChecksumValidation: 'WHEN_REQUIRED',
    })
  }

  async putObject(input: {
    key: string
    content: Buffer | Readable
    contentType: string
    metadata?: Record<string, string>
    contentLength?: number
  }): Promise<void> {
    await this.ensureBucketReady()
    await this.client.send(
      new PutObjectCommand({
        Bucket: this.bucket,
        Key: input.key,
        Body: input.content,
        ContentType: input.contentType,
        ...(input.contentLength === undefined ? {} : { ContentLength: input.contentLength }),
        ...(input.metadata ? { Metadata: sanitizeObjectMetadata(input.metadata) } : {}),
      }),
    )
  }

  async deleteObject(key: string): Promise<void> {
    await this.ensureBucketReady()
    await this.client.send(
      new DeleteObjectCommand({ Bucket: this.bucket, Key: normalizeObjectKey(key) }),
    )
  }

  async getObject(key: string): Promise<StoredObject> {
    await this.ensureBucketReady()
    const objectKey = normalizeObjectKey(key)
    const output = await this.client.send(
      new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }),
    )
    const reader = output.Body as Readable
    if (!reader) {
      throw createHttpError('文件不存在或暂不可访问', {
        statusCode: 404,
        errorCode: 'file_not_found',
      })
    }

    return {
      reader,
      contentType: String(output.ContentType || 'application/octet-stream'),
      size: Number(output.ContentLength || 0),
    }
  }

  async statObject(key: string): Promise<StoredObjectStat> {
    await this.ensureBucketReady()
    const objectKey = normalizeObjectKey(key)
    const output = await this.client.send(
      new HeadObjectCommand({ Bucket: this.bucket, Key: objectKey }),
    )
    const stat: StoredObjectStat = {
      key: objectKey,
      size: Number(output.ContentLength || 0),
      etag: String(output.ETag || ''),
      contentType: String(output.ContentType || 'application/octet-stream'),
    }
    if (output.LastModified) {
      stat.lastModified = output.LastModified
    }
    return stat
  }

  async listObjects(prefix?: string): Promise<StorageObjectSummary[]> {
    await this.ensureBucketReady()
    const objects: StorageObjectSummary[] = []
    let continuationToken: string | undefined
    do {
      const output = await this.client.send(
        new ListObjectsV2Command({
          Bucket: this.bucket,
          ...(prefix ? { Prefix: prefix } : {}),
          ContinuationToken: continuationToken,
        }),
      )
      for (const item of output.Contents || []) {
        if (!item.Key) {
          continue
        }
        const summary: StorageObjectSummary = {
          key: item.Key,
          size: Number(item.Size || 0),
          etag: String(item.ETag || ''),
        }
        if (item.LastModified) {
          summary.lastModified = item.LastModified
        }
        objects.push(summary)
      }
      continuationToken = output.IsTruncated ? output.NextContinuationToken : undefined
    } while (continuationToken)
    return objects
  }

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

    if (!this.bucketReadyPromise) {
      this.bucketReadyPromise = (async () => {
        try {
          await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }))
        } catch (error) {
          if (!isNotFoundError(error)) {
            throw error
          }
          if (!this.pathStyle) {
            throw new Error(`OSS bucket ${this.bucket} 不存在，请先在阿里云控制台创建`, {
              cause: error,
            })
          }
          await this.client.send(new CreateBucketCommand({ Bucket: this.bucket }))
        }
        this.bucketReady = true
      })()
    }
    try {
      await this.bucketReadyPromise
    } catch (error) {
      this.bucketReadyPromise = null
      throw error
    }
  }
}

export class StorageRouter {
  private readonly mode: 'minio' | 'dual' | 'oss'
  private readonly legacy: ObjectStorage
  private readonly oss: ObjectStorage | null

  constructor(mode: 'minio' | 'dual' | 'oss', legacy: ObjectStorage, oss: ObjectStorage | null) {
    this.mode = mode
    this.legacy = legacy
    this.oss = oss
    if ((mode === 'dual' || mode === 'oss') && !oss) {
      throw new Error(`存储模式 ${mode} 缺少 OSS 配置`)
    }
  }

  async putObject(input: Parameters<ObjectStorage['putObject']>[0]): Promise<void> {
    if (!Buffer.isBuffer(input.content) && this.mode === 'dual') {
      throw new Error('dual 存储模式不支持同一可读流双写，请传入 Buffer')
    }
    if (this.mode === 'minio') {
      await this.legacy.putObject(input)
      return
    }
    if (this.mode === 'oss') {
      await this.oss!.putObject(input)
      return
    }
    // 双写要求两个后端都成功，避免数据库记录指向未完成迁移的对象。
    await Promise.all([this.legacy.putObject(input), this.oss!.putObject(input)])
  }

  async getObject(key: string): Promise<StoredObject> {
    if (this.mode === 'minio') {
      return this.legacy.getObject(key)
    }
    if (this.mode === 'oss') {
      return this.oss!.getObject(key)
    }
    try {
      return await this.legacy.getObject(key)
    } catch {
      return this.oss!.getObject(key)
    }
  }

  async deleteObject(key: string): Promise<void> {
    if (this.mode === 'minio') {
      await this.legacy.deleteObject(key)
      return
    }
    if (this.mode === 'oss') {
      await this.oss!.deleteObject(key)
      return
    }
    await Promise.allSettled([this.legacy.deleteObject(key), this.oss!.deleteObject(key)])
  }
}

let storageInstance: StorageRouter | null = null

export function getObjectStorage(): StorageRouter {
  if (!storageInstance) {
    const mode = normalizeStorageMode(runtimeConfig.storage.mode)
    const legacy = new ObjectStorage(toObjectStorageConfig(runtimeConfig.storage))
    const oss =
      mode === 'minio' ? null : new ObjectStorage(toObjectStorageConfig(runtimeConfig.storage.oss))
    storageInstance = new StorageRouter(mode, legacy, oss)
  }

  return storageInstance
}

export function createConfiguredObjectStorage(config: ObjectStorageConfig): ObjectStorage {
  return new ObjectStorage(config)
}

function normalizeStorageMode(value: unknown): 'minio' | 'dual' | 'oss' {
  const mode = String(value || 'minio')
    .trim()
    .toLowerCase()
  if (mode === 'dual' || mode === 'oss') return mode
  return 'minio'
}

function toObjectStorageConfig(config: {
  endpoint: string
  bucket: string
  accessKeyId: string
  secretAccessKey: string
  region: string
}): ObjectStorageConfig {
  return {
    endpoint: config.endpoint,
    bucket: config.bucket,
    accessKeyId: config.accessKeyId,
    secretAccessKey: config.secretAccessKey,
    region: config.region,
  }
}

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:',
  }
}

function buildEndpointUrl(endpoint: { endPoint: string; port: number; useSSL: boolean }): string {
  const scheme = endpoint.useSSL ? 'https' : 'http'
  const defaultPort = endpoint.useSSL ? 443 : 80
  const suffix = endpoint.port && endpoint.port !== defaultPort ? `:${endpoint.port}` : ''
  return `${scheme}://${endpoint.endPoint}${suffix}`
}

function isNotFoundError(error: unknown): boolean {
  if (!error || typeof error !== 'object') {
    return false
  }
  const candidate = error as { name?: string; $metadata?: { httpStatusCode?: number } }
  return (
    candidate.name === 'NotFound' ||
    candidate.name === 'NoSuchBucket' ||
    candidate.$metadata?.httpStatusCode === 404
  )
}

export function normalizeObjectKey(rawKey: unknown) {
  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)),
  )
}
