import {
  BadRequestException,
  Injectable,
  NotFoundException,
  ForbiddenException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { randomUUID } from 'crypto';
import { FilesRepository } from './files.repository';
import { StorageService } from '../../infrastructure/storage/storage.service';
import { CosStorageProvider } from '../../infrastructure/storage/cos-storage.provider';
import { ContentSafetyService } from '../content-safety/content-safety.service';
import { CreateUploadUrlDto, CompleteUploadDto } from './dto';
import { EffectiveQuotaService } from '../membership/effective-quota.service';
import { EffectiveMembershipService } from '../membership/effective-membership.service';
import { PrismaService } from '../../infrastructure/database/prisma.service';

const ACTIVE_UPLOAD_SESSION_STATUSES = ['created', 'uploaded'] as const;
const UPLOAD_URL_EXPIRES_IN_SECONDS = 3600;

@Injectable()
export class FilesService {
  constructor(
    private readonly repository: FilesRepository,
    private readonly storage: StorageService,
    private readonly cos: CosStorageProvider,
    private readonly safety: ContentSafetyService,
    private readonly quotaService: EffectiveQuotaService,
    private readonly membershipService: EffectiveMembershipService,
    private readonly prisma: PrismaService,
  ) {}

  async requestUploadUrl(userId: string, dto: CreateUploadUrlDto) {
    const check = await this.safety.check(dto.filename, { userId, contentType: 'file-name' });
    if (!check.safe) {
      throw new ForbiddenException('文件名包含违规内容');
    }

    await this.cleanupExpiredUploadSessions(userId);
    await this.ensureFileSizeWithinLimit(userId, dto.sizeBytes);

    const uploadId = randomUUID();
    const objectKey = this.storage.generateObjectKey(userId, dto.filename, uploadId);
    const quotaOperationId = `storage-upload:${uploadId}`;

    await this.reserveStorageBytes({
      userId,
      uploadId,
      objectKey,
      declaredSize: dto.sizeBytes,
      quotaOperationId,
    });

    try {
      const result = await this.storage.createUploadUrl(userId, {
        filename: dto.filename,
        mimeType: dto.mimeType,
        sizeBytes: dto.sizeBytes,
        objectKey,
      }, UPLOAD_URL_EXPIRES_IN_SECONDS);

      return {
        uploadId,
        quotaOperationId,
        ...result,
      };
    } catch (error) {
      await this.repository.markUploadSessionFailed(uploadId);
      throw error;
    }
  }

  async confirmUpload(userId: string, dto: CompleteUploadDto) {
    await this.cleanupExpiredUploadSessions(userId);

    const session = await this.repository.findUploadSessionById(dto.uploadId);
    if (!session) {
      throw new NotFoundException('上传会话不存在');
    }
    if (session.userId !== userId) {
      throw new ForbiddenException('无权操作该上传会话');
    }
    if (dto.objectKey && dto.objectKey !== session.objectKey) {
      throw new BadRequestException('上传对象不匹配');
    }
    if (session.status === 'completed' && session.uploadedFile) {
      return session.uploadedFile;
    }
    if (!ACTIVE_UPLOAD_SESSION_STATUSES.includes(session.status as (typeof ACTIVE_UPLOAD_SESSION_STATUSES)[number])) {
      throw new BadRequestException('上传会话状态不可完成');
    }
    if (session.expiresAt <= new Date()) {
      await this.cleanupExpiredUploadSessions(userId);
      throw new BadRequestException('上传会话已过期');
    }

    const info = await this.storage.verifyUpload(session.objectKey);
    try {
      await this.ensureFileSizeWithinLimit(userId, info.size);
    } catch (error) {
      await this.repository.markUploadSessionFailed(session.id);
      await this.storage.deleteObject(session.objectKey).catch(() => {});
      throw error;
    }

    try {
      return await this.prisma.$transaction(async (tx) => {
        const current = await tx.uploadSession.findUnique({
          where: { id: dto.uploadId },
          include: { uploadedFile: true },
        });
        if (!current) {
          throw new NotFoundException('上传会话不存在');
        }
        if (current.userId !== userId) {
          throw new ForbiddenException('无权操作该上传会话');
        }
        if (current.status === 'completed' && current.uploadedFile) {
          return current.uploadedFile;
        }
        if (!ACTIVE_UPLOAD_SESSION_STATUSES.includes(current.status as (typeof ACTIVE_UPLOAD_SESSION_STATUSES)[number])) {
          throw new BadRequestException('上传会话状态不可完成');
        }

        const existingFile = await tx.uploadedFile.findUnique({
          where: { objectKey: current.objectKey },
        });
        if (existingFile) {
          await tx.uploadSession.update({
            where: { id: current.id },
            data: {
              status: 'completed',
              uploadedAt: current.uploadedAt ?? new Date(),
              completedAt: current.completedAt ?? new Date(),
              uploadedFileId: existingFile.id,
              reservedBytes: BigInt(info.size),
            },
          });
          return existingFile;
        }

        const storageLimit = await this.getQuotaLimit(userId, 'storage_bytes');
        const committedBytes = await this.repository.sumCommittedStorageBytes(userId, tx);
        const reservedBytes = await this.repository.sumReservedStorageBytes(userId, new Date(), current.id, tx);
        const totalAfterCommit = committedBytes + reservedBytes + info.size;

        if (storageLimit !== null && totalAfterCommit > storageLimit) {
          await tx.uploadSession.update({
            where: { id: current.id },
            data: { status: 'failed', uploadedAt: new Date() },
          });
          throw this.buildStorageLimitError(storageLimit, committedBytes + reservedBytes);
        }

        let uploadedFile;
        try {
          uploadedFile = await tx.uploadedFile.create({
            data: {
              userId,
              filename: this.extractOriginalFilename(current.objectKey),
              mimeType: info.contentType,
              storagePath: current.objectKey,
              objectKey: current.objectKey,
              bucket: this.cos.getBucket(),
              sizeBytes: info.size,
              checksum: dto.checksum,
            },
          });
        } catch (error) {
          if (!this.isUniqueConstraintError(error)) {
            throw error;
          }

          const existingAfterConflict = await tx.uploadedFile.findUnique({
            where: { objectKey: current.objectKey },
          });
          if (!existingAfterConflict) {
            throw error;
          }
          uploadedFile = existingAfterConflict;
        }

        await tx.uploadSession.update({
          where: { id: current.id },
          data: {
            status: 'completed',
            uploadedAt: new Date(),
            completedAt: new Date(),
            uploadedFileId: uploadedFile.id,
            reservedBytes: BigInt(info.size),
          },
        });

        return uploadedFile;
      }, {
        isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
      });
    } catch (error) {
      if (this.isStorageLimitError(error)) {
        await this.storage.deleteObject(session.objectKey).catch(() => {});
      } else if (!(error instanceof BadRequestException) && !(error instanceof ForbiddenException) && !(error instanceof NotFoundException)) {
        await this.repository.markUploadSessionFailed(session.id);
        await this.storage.deleteObject(session.objectKey).catch(() => {});
      }
      throw error;
    }
  }

  async getFile(userId: string, fileId: string) {
    const file = await this.repository.findById(fileId);
    if (!file) {
      throw new NotFoundException('文件不存在');
    }
    if (file.userId !== userId) {
      throw new ForbiddenException('无权访问该文件');
    }

    const downloadUrl = await this.storage.getDownloadUrl(file.objectKey!);
    return { file, downloadUrl };
  }

  async deleteFile(userId: string, fileId: string) {
    const file = await this.repository.findById(fileId);
    if (!file) {
      throw new NotFoundException('文件不存在');
    }
    if (file.userId !== userId) {
      throw new ForbiddenException('无权操作该文件');
    }

    await this.storage.deleteObject(file.objectKey!);
    await this.repository.delete(fileId);
    return { success: true };
  }

  async deleteFileAsAdmin(fileId: string) {
    const file = await this.repository.findById(fileId);
    if (!file) {
      throw new NotFoundException('文件不存在');
    }

    await this.storage.deleteObject(file.objectKey!);
    await this.repository.delete(fileId);
    return { success: true };
  }

  async findByUserId(userId: string) {
    return this.repository.findByUserId(userId);
  }

  async cleanupExpiredUploadSessions(userId?: string) {
    const now = new Date();
    const expiredSessions = await this.repository.findExpiredUploadSessions(now, userId);
    if (expiredSessions.length === 0) {
      return { expiredCount: 0, cleanedObjects: 0 };
    }

    await this.repository.expireStaleUploadSessions(now, userId);

    let cleanedObjects = 0;
    for (const session of expiredSessions) {
      try {
        await this.storage.deleteObject(session.objectKey);
        cleanedObjects += 1;
      } catch {
        // Best-effort object cleanup; session has already been marked expired.
      }
    }

    return { expiredCount: expiredSessions.length, cleanedObjects };
  }

  private async reserveStorageBytes(input: {
    userId: string;
    uploadId: string;
    objectKey: string;
    declaredSize: number;
    quotaOperationId: string;
  }) {
    const now = new Date();
    const expiresAt = new Date(now.getTime() + UPLOAD_URL_EXPIRES_IN_SECONDS * 1000);

    await this.prisma.$transaction(async (tx) => {
      const storageLimit = await this.getQuotaLimit(input.userId, 'storage_bytes');
      const committedBytes = await this.repository.sumCommittedStorageBytes(input.userId, tx);
      const reservedBytes = await this.repository.sumReservedStorageBytes(input.userId, now, undefined, tx);
      const totalAfterReserve = committedBytes + reservedBytes + input.declaredSize;

      if (storageLimit !== null && totalAfterReserve > storageLimit) {
        throw this.buildStorageLimitError(storageLimit, committedBytes + reservedBytes);
      }

      await tx.uploadSession.create({
        data: {
          id: input.uploadId,
          userId: input.userId,
          objectKey: input.objectKey,
          declaredSize: BigInt(input.declaredSize),
          reservedBytes: BigInt(input.declaredSize),
          quotaOperationId: input.quotaOperationId,
          status: 'created',
          expiresAt,
        },
      });
    }, {
      isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
    });
  }

  private async ensureFileSizeWithinLimit(userId: string, sizeBytes: number) {
    const fileSizeLimit = await this.getQuotaLimit(userId, 'file_size_bytes');
    if (fileSizeLimit !== null && sizeBytes > fileSizeLimit) {
      throw new BadRequestException({
        errorCode: 'FILE_SIZE_LIMIT_EXCEEDED',
        message: 'File too large',
        quotaType: 'file_size_bytes',
        limit: fileSizeLimit,
        used: 0,
        remaining: 0,
        resetAt: null,
        upgradeAvailable: true,
      });
    }
  }

  private async getQuotaLimit(userId: string, quotaType: 'file_size_bytes' | 'storage_bytes') {
    const quota = await this.quotaService.computeEffectiveQuota(userId);
    const limit = quota.limits.find(item => item.quotaType === quotaType);
    if (limit) {
      return limit.isUnlimited ? null : limit.limit;
    }

    const entitlements = await this.membershipService.getEntitlements(userId);
    if (!entitlements.entitlements) {
      return null;
    }

    return quotaType === 'file_size_bytes'
      ? entitlements.entitlements.maxFileSizeBytes
      : entitlements.entitlements.maxStorageBytes;
  }

  private buildStorageLimitError(limit: number, used: number) {
    return new BadRequestException({
      errorCode: 'STORAGE_LIMIT_EXCEEDED',
      message: 'Storage quota exceeded',
      quotaType: 'storage_bytes',
      limit,
      used,
      remaining: Math.max(0, limit - used),
      resetAt: null,
      upgradeAvailable: true,
    });
  }

  private isStorageLimitError(error: unknown) {
    if (!(error instanceof BadRequestException)) {
      return false;
    }
    const response = error.getResponse();
    return typeof response === 'object' && response !== null && (response as { errorCode?: string }).errorCode === 'STORAGE_LIMIT_EXCEEDED';
  }

  private isUniqueConstraintError(error: unknown) {
    return typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'P2002';
  }

  private extractOriginalFilename(objectKey: string) {
    const parts = objectKey.split('/');
    const storedName = parts[parts.length - 1] ?? objectKey;
    const firstDash = storedName.indexOf('-');
    return firstDash >= 0 ? storedName.slice(firstDash + 1) : storedName;
  }
}
