import {
  Injectable,
  Logger,
  BadRequestException,
  NotFoundException,
  InternalServerErrorException,
  ForbiddenException,
  Inject,
} from '@nestjs/common';
import * as crypto from 'crypto';
import { Document } from '../../database/models/document.model';
import { DocumentVersion } from '../../database/models/document-version.model';
import { Approval } from '../../database/models/approval.model';
import { LicenseRequest } from '../../database/models/license-request.model';
import { User } from '../../database/models/user.model';
import { MinioService } from './services/minio.service';
import { UploadDocumentDto } from './dto/upload-document.dto';
import { ApprovalStatus } from '../../common/enums';
import { AuditService } from '../audit/audit.service';

@Injectable()
export class DocumentsService {
  private readonly logger = new Logger(DocumentsService.name);
  private readonly MAX_FILE_SIZE = 10 * 1024 * 1024;
  private readonly ALLOWED_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
  private readonly ALLOWED_DOC_TYPES = [
    'FLOOR_PLAN',
    'PHOTOGRAPH',
    'ID_PROOF',
    'ADDRESS_PROOF',
    'NOC',
    'LICENSE_COPY',
    'OTHER',
    'FIRE_SAFETY',
    'HEALTH_CERT',
    'TAX_CLEARANCE',
    'SITE_PLAN',
    'BUILDING_PERMIT',
    'BUSINESS_LICENSE',
  ];

  constructor(
    @Inject(Document)
    private readonly documentRepository: typeof Document,
    @Inject(DocumentVersion)
    private readonly documentVersionRepository: typeof DocumentVersion,
    @Inject(Approval)
    private readonly approvalRepository: typeof Approval,
    @Inject(LicenseRequest)
    private readonly requestRepository: typeof LicenseRequest,
    @Inject(User)
    private readonly userRepository: typeof User,
    private readonly minioService: MinioService,
    private readonly auditService: AuditService,
  ) {}

  private sanitizeFilename(filename: string): string {
    // Remove path traversal attempts and dangerous characters
    return filename
      .replace(/\.\./g, '')
      .replace(/[<>:"|?*]/g, '')
      .replace(/DROP\s+TABLE/gi, '')
      .replace(/script/gi, '')
      .replace(/^\s+|\s+$/g, '')
      .replace(/\//g, '_')
      .replace(/\\/g, '_');
  }

  async checkUserCanAccessRequest(
    userId: string,
    requestId: string,
  ): Promise<{ canAccess: boolean; isAdmin: boolean }> {
    const user = await this.userRepository.query().findById(userId);
    if (!user) {
      return { canAccess: false, isAdmin: false };
    }

    const isAdmin = user.role === 'ADMIN';
    if (isAdmin) {
      return { canAccess: true, isAdmin: true };
    }

    const request = await this.requestRepository
      .query()
      .findById(requestId)
      .withGraphFetched('applicant');

    if (!request) {
      return { canAccess: false, isAdmin: false };
    }

    // Check if user is the owner (applicant) - linked by email
    const applicant = request.applicant as any;
    if (applicant && applicant.email === user.email) {
      return { canAccess: true, isAdmin: false };
    }

    // Check if user is from an assigned department
    if (user.role === 'DEPARTMENT' && user.departmentId) {
      const approvals = await this.approvalRepository
        .query()
        .where({ request_id: requestId, department_id: user.departmentId });
      if (approvals.length > 0) {
        return { canAccess: true, isAdmin: false };
      }
    }

    return { canAccess: false, isAdmin: false };
  }

  async upload(
    requestId: string,
    file: Express.Multer.File,
    dto: UploadDocumentDto,
    userId: string,
  ): Promise<Document> {
    this.logger.debug(`Uploading document for request: ${requestId}, docType: ${dto.docType}`);

    await this.validateFileUpload(file);
    this.validateDocumentType(dto.docType);

    const request = await this.requestRepository.query().findById(requestId);

    if (!request) {
      throw new NotFoundException(`Request not found: ${requestId}`);
    }

    // Check authorization
    const { canAccess } = await this.checkUserCanAccessRequest(userId, requestId);
    if (!canAccess) {
      throw new ForbiddenException({
        code: 'AUTH_008',
        message: 'You do not have permission to upload documents to this request',
      });
    }

    // Sanitize filename
    const sanitizedFilename = this.sanitizeFilename(file.originalname);

    try {
      const fileHash = this.generateHash(file.buffer);
      const bucket = this.minioService.getDefaultBucket();
      const filePath = `requests/${requestId}/${dto.docType}/${Date.now()}-${sanitizedFilename}`;

      await this.minioService.uploadFile(bucket, filePath, file.buffer, {
        'Content-Type': file.mimetype,
        'original-filename': sanitizedFilename,
        'doc-type': dto.docType,
        'uploaded-by': userId,
      });

      const document = await this.documentRepository
        .query()
        .findOne({ request_id: requestId, doc_type: dto.docType });

      let savedDocument;
      let currentVersion = 1;

      if (document) {
        currentVersion = document.currentVersion + 1;
        savedDocument = await document.$query().patchAndFetch({
          currentVersion,
          currentHash: fileHash,
          originalFilename: sanitizedFilename,
        });
      } else {
        savedDocument = await this.documentRepository.query().insertAndFetch({
          requestId,
          docType: dto.docType,
          originalFilename: sanitizedFilename,
          currentVersion: 1,
          currentHash: fileHash,
          minioBucket: bucket,
          isActive: true,
        });
      }

      await this.documentVersionRepository.query().insert({
        documentId: savedDocument.id,
        version: currentVersion,
        hash: fileHash,
        minioPath: filePath,
        fileSize: file.size.toString(),
        mimeType: file.mimetype,
        uploadedBy: userId,
      });

      const invalidatedDepartments = await this.getInvalidatedApprovals(savedDocument.id);
      if (invalidatedDepartments.length > 0) {
        this.logger.log(
          `Document upload invalidated approvals for departments: ${invalidatedDepartments.join(', ')}`,
        );
      }

      this.logger.log(
        `Document uploaded successfully: ${savedDocument.id}, version: ${currentVersion}`,
      );

      // Record audit log
      await this.auditService.record({
        entityType: 'REQUEST',
        entityId: requestId,
        action: 'DOCUMENT_UPLOADED',
        actorType: 'USER',
        actorId: userId,
        newValue: {
          documentId: savedDocument.id,
          docType: dto.docType,
          filename: sanitizedFilename,
          version: currentVersion,
          performedBy: userId,
          reason: `Document ${savedDocument.id} uploaded: ${sanitizedFilename}`,
        },
      });

      return savedDocument;
    } catch (error) {
      this.logger.error(`Failed to upload document: ${error.message}`);
      throw new InternalServerErrorException('Failed to upload document');
    }
  }

  async findById(id: string): Promise<Document> {
    this.logger.debug(`Finding document: ${id}`);

    const document = await this.documentRepository
      .query()
      .findById(id)
      .withGraphFetched('versions');

    if (!document) {
      throw new NotFoundException(`Document not found: ${id}`);
    }

    return document;
  }

  async findByRequestId(requestId: string): Promise<Document[]> {
    this.logger.debug(`Finding documents for request: ${requestId}`);

    return await this.documentRepository
      .query()
      .where({ request_id: requestId, is_active: true })
      .withGraphFetched('versions')
      .orderBy('created_at', 'DESC');
  }

  async getVersions(documentId: string): Promise<DocumentVersion[]> {
    this.logger.debug(`Fetching versions for document: ${documentId}`);

    await this.findById(documentId);

    return await this.documentVersionRepository
      .query()
      .where({ document_id: documentId })
      .orderBy('version', 'DESC');
  }

  async uploadNewVersion(
    requestId: string,
    documentId: string,
    file: Express.Multer.File,
    userId: string,
  ): Promise<Document> {
    this.logger.debug(`Uploading new version for document: ${documentId}`);

    const document = await this.findById(documentId);

    if (document.requestId !== requestId) {
      throw new BadRequestException('Document does not belong to the specified request');
    }

    return this.upload(requestId, file, { docType: document.docType }, userId);
  }

  async getDownloadUrl(
    documentId: string,
    version?: number,
  ): Promise<{ url: string; expiresAt: Date }> {
    this.logger.debug(`Generating download URL for document: ${documentId}, version: ${version}`);

    const document = await this.findById(documentId);

    let targetVersion: DocumentVersion;

    if (version) {
      targetVersion = await this.documentVersionRepository
        .query()
        .findOne({ document_id: documentId, version });

      if (!targetVersion) {
        throw new NotFoundException(`Document version not found: ${version}`);
      }
    } else {
      targetVersion = await this.documentVersionRepository
        .query()
        .where({ document_id: documentId })
        .orderBy('version', 'DESC')
        .first();

      if (!targetVersion) {
        throw new NotFoundException('No versions found for document');
      }
    }

    const bucket = document.minioBucket;
    const expiresIn = 3600;
    const url = await this.minioService.getSignedUrl(bucket, targetVersion.minioPath, expiresIn);
    const expiresAt = new Date(Date.now() + expiresIn * 1000);

    this.logger.debug(`Download URL generated successfully`);

    return { url, expiresAt };
  }

  async getFileContent(
    documentId: string,
    version?: number,
    userId?: string,
  ): Promise<{
    buffer: Buffer;
    mimeType: string;
    fileName: string;
    fileSize: number;
    hash: string;
  }> {
    this.logger.debug(`Getting file content for document: ${documentId}, version: ${version}`);

    const document = await this.findById(documentId);

    let targetVersion: DocumentVersion;

    if (version) {
      targetVersion = await this.documentVersionRepository
        .query()
        .findOne({ document_id: documentId, version });

      if (!targetVersion) {
        throw new NotFoundException(`Document version not found: ${version}`);
      }
    } else {
      targetVersion = await this.documentVersionRepository
        .query()
        .where({ document_id: documentId })
        .orderBy('version', 'DESC')
        .first();

      if (!targetVersion) {
        throw new NotFoundException('No versions found for document');
      }
    }

    const bucket = document.minioBucket;

    // Validate that we have the required path information
    if (!targetVersion.minioPath) {
      this.logger.error(`Document version ${targetVersion.id} has no minioPath`);
      throw new NotFoundException('Document file path not found');
    }

    if (!bucket) {
      this.logger.error(`Document ${documentId} has no minioBucket`);
      throw new NotFoundException('Document storage bucket not found');
    }

    let buffer: Buffer;
    try {
      buffer = await this.minioService.getFile(bucket, targetVersion.minioPath);
    } catch (error: any) {
      // Re-throw NotFoundException with more context
      if (error instanceof NotFoundException) {
        this.logger.error(
          `File not found in storage for document ${documentId}: ${targetVersion.minioPath}`,
        );
        throw new NotFoundException(
          `Document file not found in storage. The file may have been deleted or moved.`,
        );
      }
      // Log and re-throw other errors
      this.logger.error(`Failed to retrieve document ${documentId} from storage: ${error.message}`);
      throw error;
    }

    // Track download
    const currentCount = document.downloadCount || 0;
    try {
      await document.$query().patch({
        downloadCount: currentCount + 1,
        lastDownloadedAt: new Date().toISOString(),
      });
    } catch (patchError: any) {
      // Log but don't fail the download for tracking errors
      this.logger.warn(
        `Failed to update download tracking for document ${documentId}: ${patchError.message}`,
      );
    }

    // Record audit log for download
    if (userId) {
      try {
        await this.auditService.record({
          entityType: 'REQUEST',
          entityId: document.requestId,
          action: 'DOCUMENT_DOWNLOADED',
          actorType: 'USER',
          actorId: userId,
          newValue: {
            documentId: documentId,
            filename: document.originalFilename,
            version: targetVersion.version,
            performedBy: userId,
            reason: `Document ${documentId} downloaded: ${document.originalFilename}`,
          },
        });
      } catch (auditError: any) {
        // Log but don't fail the download for audit errors
        this.logger.warn(
          `Failed to record audit log for document download ${documentId}: ${auditError.message}`,
        );
      }
    }

    return {
      buffer,
      mimeType: targetVersion.mimeType || 'application/octet-stream',
      fileName: document.originalFilename,
      fileSize: parseInt(targetVersion.fileSize) || buffer.length,
      hash: targetVersion.hash,
    };
  }

  async checkUserCanAccessDocument(userId: string, documentId: string): Promise<boolean> {
    const document = await this.findById(documentId);
    const { canAccess } = await this.checkUserCanAccessRequest(userId, document.requestId);
    return canAccess;
  }

  async softDelete(documentId: string): Promise<void> {
    this.logger.debug(`Soft deleting document: ${documentId}`);

    const document = await this.findById(documentId);

    await document.$query().patch({ isActive: false });

    const invalidatedDepartments = await this.getInvalidatedApprovals(documentId);
    if (invalidatedDepartments.length > 0) {
      this.logger.log(
        `Document deletion invalidated approvals for departments: ${invalidatedDepartments.join(', ')}`,
      );
    }

    this.logger.log(`Document soft deleted: ${documentId}`);
  }

  async getInvalidatedApprovals(documentId: string): Promise<string[]> {
    this.logger.debug(`Getting invalidated approvals for document: ${documentId}`);

    const document = await this.findById(documentId);

    const approvals = await this.approvalRepository
      .query()
      .where({
        request_id: document.requestId,
        status: ApprovalStatus.APPROVED,
      })
      .whereNull('invalidated_at')
      .withGraphFetched('department');

    const invalidatedDepartments: string[] = [];

    for (const approval of approvals) {
      if ((approval.reviewedDocuments as any)?.includes(documentId)) {
        await approval.$query().patch({
          invalidatedAt: new Date(),
          invalidationReason: `Document invalidated: ${documentId}`,
        });
        invalidatedDepartments.push((approval.department as any)?.code || approval.departmentId);
      }
    }

    return invalidatedDepartments;
  }

  generateHash(buffer: Buffer): string {
    return crypto.createHash('sha256').update(buffer).digest('hex');
  }

  validateFileType(mimeType: string): boolean {
    return this.ALLOWED_MIME_TYPES.includes(mimeType);
  }

  validateFileSize(size: number): boolean {
    return size <= this.MAX_FILE_SIZE;
  }

  private validateDocumentType(docType: string): void {
    if (!this.ALLOWED_DOC_TYPES.includes(docType)) {
      throw new BadRequestException(
        `Invalid document type: ${docType}. Allowed types: ${this.ALLOWED_DOC_TYPES.join(', ')}`,
      );
    }
  }

  private async validateFileUpload(file: Express.Multer.File): Promise<void> {
    if (!file) {
      throw new BadRequestException('No file provided');
    }

    if (!this.validateFileSize(file.size)) {
      throw new BadRequestException(
        `File size exceeds maximum limit of ${this.MAX_FILE_SIZE / 1024 / 1024}MB`,
      );
    }

    if (!this.validateFileType(file.mimetype)) {
      throw new BadRequestException(
        `Invalid file type. Allowed types: ${this.ALLOWED_MIME_TYPES.join(', ')}`,
      );
    }
  }
}
