/**
 * StorageService - Gestión de almacenamiento de archivos
 *
 * Servicio para subir, descargar y gestionar archivos.
 * Soporta múltiples providers (local, S3, GCS, Azure).
 *
 * @module Storage
 */

import { DataSource, Repository } from 'typeorm';
import { StorageFile, StorageBucket, StorageUpload, TenantUsage, StorageProvider } from '../entities';
import * as crypto from 'crypto';
import * as path from 'path';

interface ServiceContext {
  tenantId: string;
  userId?: string;
}

export interface CreateFileDto {
  bucketId: string;
  folderId?: string;
  name: string;
  originalName: string;
  mimeType: string;
  sizeBytes: number;
  storageKey: string;
  checksumMd5?: string;
  checksumSha256?: string;
  metadata?: Record<string, any>;
  tags?: string[];
  entityType?: string;
  entityId?: string;
  isPublic?: boolean;
}

export interface FileFilters {
  bucketId?: string;
  folderId?: string;
  mimeType?: string;
  search?: string;
  status?: string;
  entityType?: string;
  entityId?: string;
  createdFrom?: Date;
  createdTo?: Date;
  page?: number;
  limit?: number;
}

interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export class StorageService {
  private fileRepo: Repository<StorageFile>;
  private uploadRepo: Repository<StorageUpload>;
  private bucketRepo: Repository<StorageBucket>;
  private usageRepo: Repository<TenantUsage>;

  constructor(dataSource: DataSource) {
    this.fileRepo = dataSource.getRepository(StorageFile);
    this.uploadRepo = dataSource.getRepository(StorageUpload);
    this.bucketRepo = dataSource.getRepository(StorageBucket);
    this.usageRepo = dataSource.getRepository(TenantUsage);
  }

  /**
   * Create a file record after upload
   */
  async createFile(ctx: ServiceContext, dto: CreateFileDto): Promise<StorageFile> {
    const uniqueName = dto.name || this.generateUniqueFileName(dto.originalName);
    const ext = path.extname(dto.originalName).substring(1).toLowerCase();
    const category = this.categorizeFile(dto.mimeType);

    const file = this.fileRepo.create({
      tenantId: ctx.tenantId,
      bucketId: dto.bucketId,
      folderId: dto.folderId,
      name: uniqueName,
      originalName: dto.originalName,
      path: dto.storageKey,
      mimeType: dto.mimeType,
      extension: ext,
      category,
      sizeBytes: dto.sizeBytes,
      storageKey: dto.storageKey,
      checksumMd5: dto.checksumMd5,
      checksumSha256: dto.checksumSha256,
      metadata: dto.metadata || {},
      tags: dto.tags || [],
      entityType: dto.entityType,
      entityId: dto.entityId,
      isPublic: dto.isPublic || false,
      uploadedBy: ctx.userId,
      status: 'active',
    });

    const savedFile = await this.fileRepo.save(file);

    // Update tenant usage
    await this.updateTenantUsage(ctx.tenantId, dto.bucketId, dto.sizeBytes, 1);

    return savedFile;
  }

  /**
   * Find file by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<StorageFile | null> {
    return this.fileRepo.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        status: 'active',
      },
    });
  }

  /**
   * Find files with filters
   */
  async findAll(
    ctx: ServiceContext,
    filters: FileFilters = {}
  ): Promise<PaginatedResult<StorageFile>> {
    const page = filters.page || 1;
    const limit = Math.min(filters.limit || 20, 100);
    const skip = (page - 1) * limit;

    const qb = this.fileRepo
      .createQueryBuilder('f')
      .where('f.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('f.status = :status', { status: filters.status || 'active' });

    if (filters.bucketId) {
      qb.andWhere('f.bucket_id = :bucketId', { bucketId: filters.bucketId });
    }
    if (filters.folderId) {
      qb.andWhere('f.folder_id = :folderId', { folderId: filters.folderId });
    }
    if (filters.mimeType) {
      qb.andWhere('f.mime_type LIKE :mimeType', { mimeType: `${filters.mimeType}%` });
    }
    if (filters.entityType) {
      qb.andWhere('f.entity_type = :entityType', { entityType: filters.entityType });
    }
    if (filters.entityId) {
      qb.andWhere('f.entity_id = :entityId', { entityId: filters.entityId });
    }
    if (filters.search) {
      qb.andWhere('(f.name ILIKE :search OR f.original_name ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }
    if (filters.createdFrom) {
      qb.andWhere('f.created_at >= :createdFrom', { createdFrom: filters.createdFrom });
    }
    if (filters.createdTo) {
      qb.andWhere('f.created_at <= :createdTo', { createdTo: filters.createdTo });
    }

    qb.orderBy('f.created_at', 'DESC').skip(skip).take(limit);

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Generate upload URL (for direct uploads)
   */
  async generateUploadUrl(
    ctx: ServiceContext,
    bucketId: string,
    fileName: string,
    mimeType: string,
    sizeBytes: number
  ): Promise<{ uploadId: string; uploadUrl: string; expiresAt: Date }> {
    const bucket = await this.bucketRepo.findOne({ where: { id: bucketId, isActive: true } });
    if (!bucket) {
      throw new Error('Bucket not found or inactive');
    }

    const uploadId = crypto.randomUUID();
    const expiresAt = new Date();
    expiresAt.setHours(expiresAt.getHours() + 1);

    const upload = this.uploadRepo.create({
      id: uploadId,
      tenantId: ctx.tenantId,
      bucketId,
      fileName,
      mimeType,
      totalSizeBytes: sizeBytes,
      status: 'pending',
      expiresAt,
      createdBy: ctx.userId,
    });

    await this.uploadRepo.save(upload);

    // Generate presigned URL based on provider
    const uploadUrl = await this.generatePresignedUrl(bucket, fileName, mimeType, uploadId);

    return { uploadId, uploadUrl, expiresAt };
  }

  /**
   * Complete upload after file is received
   */
  async completeUpload(
    ctx: ServiceContext,
    uploadId: string,
    storageKey: string,
    actualSize: number,
    checksum?: string
  ): Promise<StorageFile> {
    const upload = await this.uploadRepo.findOne({
      where: { id: uploadId, tenantId: ctx.tenantId },
    });

    if (!upload) {
      throw new Error('Upload not found');
    }

    if (upload.status !== 'pending' && upload.status !== 'uploading') {
      throw new Error(`Upload is ${upload.status}, cannot complete`);
    }

    // Create file record
    const file = await this.createFile(ctx, {
      bucketId: upload.bucketId,
      folderId: upload.folderId,
      name: upload.fileName,
      originalName: upload.fileName,
      mimeType: upload.mimeType || 'application/octet-stream',
      sizeBytes: actualSize,
      storageKey,
      checksumSha256: checksum,
    });

    // Update upload status
    await this.uploadRepo.update(
      { id: uploadId },
      { status: 'completed', completedAt: new Date(), fileId: file.id }
    );

    return file;
  }

  /**
   * Generate download URL for a file
   */
  async generateDownloadUrl(
    ctx: ServiceContext,
    fileId: string,
    expiresInMinutes: number = 60
  ): Promise<string> {
    const file = await this.findById(ctx, fileId);
    if (!file) {
      throw new Error('File not found');
    }

    const bucket = await this.bucketRepo.findOne({ where: { id: file.bucketId } });
    if (!bucket) {
      throw new Error('Bucket not found');
    }

    return this.generateDownloadPresignedUrl(bucket, file.storageKey, expiresInMinutes);
  }

  /**
   * Soft delete a file
   */
  async deleteFile(ctx: ServiceContext, fileId: string): Promise<boolean> {
    const file = await this.findById(ctx, fileId);
    if (!file) {
      return false;
    }

    await this.fileRepo.update(
      { id: fileId, tenantId: ctx.tenantId },
      { status: 'deleted', deletedAt: new Date() }
    );

    // Update tenant usage (subtract file size)
    await this.updateTenantUsage(ctx.tenantId, file.bucketId, -file.sizeBytes, -1);

    return true;
  }

  /**
   * Get tenant storage usage
   */
  async getTenantUsage(ctx: ServiceContext, bucketId?: string): Promise<{
    totalSizeBytes: number;
    fileCount: number;
    quotaBytes: number | null;
    usagePercentage: number;
  }> {
    const monthYear = this.getCurrentMonthYear();

    const whereClause: Record<string, any> = {
      tenantId: ctx.tenantId,
      monthYear,
    };

    if (bucketId) {
      whereClause.bucketId = bucketId;
    }

    const usages = await this.usageRepo.find({ where: whereClause });

    if (usages.length === 0) {
      return {
        totalSizeBytes: 0,
        fileCount: 0,
        quotaBytes: null,
        usagePercentage: 0,
      };
    }

    const totalSizeBytes = usages.reduce((sum, u) => sum + (Number(u.totalSizeBytes) || 0), 0);
    const fileCount = usages.reduce((sum, u) => sum + (u.fileCount || 0), 0);
    const quotaBytes = usages[0].quotaBytes ? Number(usages[0].quotaBytes) : null;

    return {
      totalSizeBytes,
      fileCount,
      quotaBytes,
      usagePercentage: quotaBytes ? (totalSizeBytes / quotaBytes) * 100 : 0,
    };
  }

  /**
   * Copy file
   */
  async copyFile(
    ctx: ServiceContext,
    sourceFileId: string,
    targetFolderId?: string
  ): Promise<StorageFile> {
    const sourceFile = await this.findById(ctx, sourceFileId);
    if (!sourceFile) {
      throw new Error('Source file not found');
    }

    const newStorageKey = this.generateStoragePath(sourceFile.originalName);

    return this.createFile(ctx, {
      bucketId: sourceFile.bucketId,
      folderId: targetFolderId || sourceFile.folderId,
      name: this.generateUniqueFileName(sourceFile.originalName),
      originalName: sourceFile.originalName,
      mimeType: sourceFile.mimeType,
      sizeBytes: sourceFile.sizeBytes,
      storageKey: newStorageKey,
      checksumMd5: sourceFile.checksumMd5,
      checksumSha256: sourceFile.checksumSha256,
      metadata: { ...sourceFile.metadata, copiedFrom: sourceFileId },
      tags: [...sourceFile.tags],
    });
  }

  // ============ Private Helper Methods ============

  private generateUniqueFileName(originalName: string): string {
    const ext = path.extname(originalName);
    const base = path.basename(originalName, ext);
    const timestamp = Date.now();
    const random = crypto.randomBytes(4).toString('hex');
    return `${base}_${timestamp}_${random}${ext}`;
  }

  private generateStoragePath(fileName: string): string {
    const date = new Date();
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}/${month}/${day}/${this.generateUniqueFileName(fileName)}`;
  }

  private getCurrentMonthYear(): string {
    const date = new Date();
    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
  }

  private categorizeFile(mimeType: string): 'image' | 'document' | 'video' | 'audio' | 'archive' | 'other' {
    if (mimeType.startsWith('image/')) return 'image';
    if (mimeType.startsWith('video/')) return 'video';
    if (mimeType.startsWith('audio/')) return 'audio';
    if (mimeType.includes('pdf') || mimeType.includes('document') || mimeType.includes('spreadsheet') || mimeType.includes('text/')) return 'document';
    if (mimeType.includes('zip') || mimeType.includes('tar') || mimeType.includes('rar') || mimeType.includes('7z')) return 'archive';
    return 'other';
  }

  private async updateTenantUsage(
    tenantId: string,
    bucketId: string,
    sizeChange: number,
    countChange: number
  ): Promise<void> {
    const monthYear = this.getCurrentMonthYear();

    let usage = await this.usageRepo.findOne({
      where: { tenantId, bucketId, monthYear },
    });

    if (!usage) {
      usage = this.usageRepo.create({
        tenantId,
        bucketId,
        monthYear,
        totalSizeBytes: Math.max(0, sizeChange),
        fileCount: Math.max(0, countChange),
      });
    } else {
      usage.totalSizeBytes = Math.max(0, Number(usage.totalSizeBytes || 0) + sizeChange);
      usage.fileCount = Math.max(0, (usage.fileCount || 0) + countChange);
    }

    await this.usageRepo.save(usage);
  }

  private async generatePresignedUrl(
    bucket: StorageBucket,
    fileName: string,
    _mimeType: string,
    uploadId: string
  ): Promise<string> {
    const provider = bucket.storageProvider as StorageProvider;
    const config = bucket.storageConfig || {};

    switch (provider) {
      case 's3':
        return `https://${config.bucket || 'default'}.s3.${config.region || 'us-east-1'}.amazonaws.com/uploads/${fileName}`;
      case 'gcs':
        return `https://storage.googleapis.com/${config.bucket || 'default'}/uploads/${fileName}`;
      case 'azure':
        return `https://${config.account || 'default'}.blob.core.windows.net/${config.container || 'uploads'}/${fileName}`;
      default:
        // Local: return internal upload endpoint
        return `/api/storage/upload/${uploadId}`;
    }
  }

  private async generateDownloadPresignedUrl(
    bucket: StorageBucket,
    storageKey: string,
    _expiresInMinutes: number
  ): Promise<string> {
    const provider = bucket.storageProvider as StorageProvider;
    const config = bucket.storageConfig || {};

    switch (provider) {
      case 's3':
        return `https://${config.bucket || 'default'}.s3.${config.region || 'us-east-1'}.amazonaws.com/${storageKey}`;
      case 'gcs':
        return `https://storage.googleapis.com/${config.bucket || 'default'}/${storageKey}`;
      case 'azure':
        return `https://${config.account || 'default'}.blob.core.windows.net/${config.container || 'uploads'}/${storageKey}`;
      default:
        return `/api/storage/download/${storageKey}`;
    }
  }
}
