import { FastifyReply, FastifyRequest } from 'fastify';
import { DocumentsService } from '../domain/documents.service';
import type { CreateBody, IdParams, ListQuery, UpdateBody } from './documents.validation';
import { getStorageService } from '../../../core/storage/storage.service';
import { logger } from '../../../core/logging/logger';
import path from 'path';
import { Transform, TransformCallback } from 'stream';
import crypto from 'crypto';
import FileType from 'file-type';
import { Readable } from 'stream';
import { canAccessFeature, getFeatureConfig } from '../../../core/config/feature-tiers';
import { SubscriptionTier } from '../../user-profile/domain/user-profile.types';

export class DocumentsController {
  private readonly service = new DocumentsService();

  async list(request: FastifyRequest<{ Querystring: ListQuery }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;

    logger.info('Documents list requested', {
      operation: 'documents.list',
      userId,
      filters: {
        vehicleId: request.query.vehicleId,
        type: request.query.type,
        expiresBefore: request.query.expiresBefore,
      },
    });

    const docs = await this.service.listDocuments(userId, {
      vehicleId: request.query.vehicleId,
      type: request.query.type,
      expiresBefore: request.query.expiresBefore,
    });

    logger.info('Documents list retrieved', {
      operation: 'documents.list.success',
      userId,
      documentCount: docs.length,
    });

    return reply.code(200).send(docs);
  }

  async get(request: FastifyRequest<{ Params: IdParams }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const documentId = request.params.id;

    logger.info('Document get requested', {
      operation: 'documents.get',
      userId,
      documentId,
    });

    const doc = await this.service.getDocument(userId, documentId);
    if (!doc) {
      logger.warn('Document not found', {
        operation: 'documents.get.not_found',
        userId,
        documentId,
      });
      return reply.code(404).send({ error: 'Not Found' });
    }

    logger.info('Document retrieved', {
      operation: 'documents.get.success',
      userId,
      documentId,
      vehicleId: doc.vehicleId,
      documentType: doc.documentType,
    });

    return reply.code(200).send(doc);
  }

  async create(request: FastifyRequest<{ Body: CreateBody }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const userTier: SubscriptionTier = request.userContext?.subscriptionTier || 'free';

    logger.info('Document create requested', {
      operation: 'documents.create',
      userId,
      vehicleId: request.body.vehicleId,
      documentType: request.body.documentType,
      title: request.body.title,
    });

    // Tier validation: scanForMaintenance requires Pro tier
    const featureKey = 'document.scanMaintenanceSchedule';
    if (request.body.scanForMaintenance && !canAccessFeature(userTier, featureKey)) {
      const config = getFeatureConfig(featureKey);
      logger.warn('Tier required for scanForMaintenance', {
        operation: 'documents.create.tier_required',
        userId,
        userTier,
        requiredTier: config?.minTier,
      });
      return reply.code(403).send({
        error: 'TIER_REQUIRED',
        requiredTier: config?.minTier || 'pro',
        currentTier: userTier,
        feature: featureKey,
        featureName: config?.name || null,
        upgradePrompt: config?.upgradePrompt || 'Upgrade to Pro to access this feature.',
      });
    }

    const created = await this.service.createDocument(userId, request.body);

    logger.info('Document created', {
      operation: 'documents.create.success',
      userId,
      documentId: created.id,
      vehicleId: created.vehicleId,
      documentType: created.documentType,
      title: created.title,
    });

    return reply.code(201).send(created);
  }

  async update(request: FastifyRequest<{ Params: IdParams; Body: UpdateBody }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const userTier: SubscriptionTier = request.userContext?.subscriptionTier || 'free';
    const documentId = request.params.id;

    logger.info('Document update requested', {
      operation: 'documents.update',
      userId,
      documentId,
      updateFields: Object.keys(request.body),
    });

    // Tier validation: scanForMaintenance requires Pro tier
    const featureKey = 'document.scanMaintenanceSchedule';
    if (request.body.scanForMaintenance && !canAccessFeature(userTier, featureKey)) {
      const config = getFeatureConfig(featureKey);
      logger.warn('Tier required for scanForMaintenance', {
        operation: 'documents.update.tier_required',
        userId,
        documentId,
        userTier,
        requiredTier: config?.minTier,
      });
      return reply.code(403).send({
        error: 'TIER_REQUIRED',
        requiredTier: config?.minTier || 'pro',
        currentTier: userTier,
        feature: featureKey,
        featureName: config?.name || null,
        upgradePrompt: config?.upgradePrompt || 'Upgrade to Pro to access this feature.',
      });
    }

    const updated = await this.service.updateDocument(userId, documentId, request.body);
    if (!updated) {
      logger.warn('Document not found for update', {
        operation: 'documents.update.not_found',
        userId,
        documentId,
      });
      return reply.code(404).send({ error: 'Not Found' });
    }

    logger.info('Document updated', {
      operation: 'documents.update.success',
      userId,
      documentId,
      vehicleId: updated.vehicleId,
      title: updated.title,
    });

    return reply.code(200).send(updated);
  }

  async remove(request: FastifyRequest<{ Params: IdParams }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const documentId = request.params.id;

    logger.info('Document delete requested', {
      operation: 'documents.delete',
      userId,
      documentId,
    });

    // If object exists, delete it from storage first
    const existing = await this.service.getDocument(userId, documentId);
    if (existing && existing.storageBucket && existing.storageKey) {
      const storage = getStorageService();
      try {
        await storage.deleteObject(existing.storageBucket, existing.storageKey);
        logger.info('Document file deleted from storage', {
          operation: 'documents.delete.storage_cleanup',
          userId,
          documentId,
          storageKey: existing.storageKey,
        });
      } catch (e) {
        logger.warn('Failed to delete document file from storage', {
          operation: 'documents.delete.storage_cleanup_failed',
          userId,
          documentId,
          storageKey: existing.storageKey,
          error: e instanceof Error ? e.message : 'Unknown error',
        });
        // Non-fatal: proceed with soft delete
      }
    }

    await this.service.deleteDocument(userId, documentId);

    logger.info('Document deleted', {
      operation: 'documents.delete.success',
      userId,
      documentId,
      vehicleId: existing?.vehicleId,
      hadFile: !!(existing?.storageKey),
    });

    return reply.code(204).send();
  }

  async upload(request: FastifyRequest<{ Params: IdParams }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const documentId = request.params.id;

    logger.info('Document upload requested', {
      operation: 'documents.upload',
      userId,
      documentId,
    });

    const doc = await this.service.getDocument(userId, documentId);
    if (!doc) {
      logger.warn('Document not found for upload', {
        operation: 'documents.upload.not_found',
        userId,
        documentId,
      });
      return reply.code(404).send({ error: 'Not Found' });
    }

    const mp = await (request as any).file({ limits: { files: 1 } });
    if (!mp) {
      logger.warn('No file provided for upload', {
        operation: 'documents.upload.no_file',
        userId,
        documentId,
      });
      return reply.code(400).send({ error: 'Bad Request', message: 'No file provided' });
    }

    // Define allowed MIME types and their corresponding magic byte signatures
    const allowedTypes = new Map([
      ['application/pdf', new Set(['application/pdf'])],
      ['image/jpeg', new Set(['image/jpeg'])],
      ['image/png', new Set(['image/png'])],
    ]);

    const contentType = mp.mimetype as string | undefined;
    if (!contentType || !allowedTypes.has(contentType)) {
      logger.warn('Unsupported file type for upload (header validation)', {
        operation: 'documents.upload.unsupported_type',
        userId,
        documentId,
        contentType,
        fileName: mp.filename,
      });
      return reply.code(415).send({
        error: 'Unsupported Media Type',
        message: 'Only PDF, JPEG, and PNG files are allowed'
      });
    }

    // Collect ALL file chunks first (breaking early from async iterator corrupts stream state)
    const chunks: Buffer[] = [];
    for await (const chunk of mp.file) {
      chunks.push(chunk);
    }
    const fullBuffer = Buffer.concat(chunks);

    // Use first 4100 bytes for file type detection via magic bytes
    const headerBuffer = fullBuffer.subarray(0, Math.min(4100, fullBuffer.length));

    // Validate actual file content using magic bytes
    const detectedType = await FileType.fromBuffer(headerBuffer);

    if (!detectedType) {
      logger.warn('Unable to detect file type from content', {
        operation: 'documents.upload.type_detection_failed',
        userId,
        documentId,
        contentType,
        fileName: mp.filename,
      });
      return reply.code(415).send({
        error: 'Unsupported Media Type',
        message: 'Unable to verify file type from content'
      });
    }

    // Verify detected type matches claimed Content-Type
    const allowedDetectedTypes = allowedTypes.get(contentType);
    if (!allowedDetectedTypes || !allowedDetectedTypes.has(detectedType.mime)) {
      logger.warn('File content does not match Content-Type header', {
        operation: 'documents.upload.type_mismatch',
        userId,
        documentId,
        claimedType: contentType,
        detectedType: detectedType.mime,
        fileName: mp.filename,
      });
      return reply.code(415).send({
        error: 'Unsupported Media Type',
        message: `File content (${detectedType.mime}) does not match claimed type (${contentType})`
      });
    }

    const originalName: string = mp.filename || 'upload';
    const ext = (() => {
      const e = path.extname(originalName).replace(/^\./, '').toLowerCase();
      if (e) return e;
      if (contentType === 'application/pdf') return 'pdf';
      if (contentType === 'image/jpeg') return 'jpg';
      if (contentType === 'image/png') return 'png';
      return 'bin';
    })();

    class CountingStream extends Transform {
      public bytes = 0;
      override _transform(chunk: any, _enc: BufferEncoding, cb: TransformCallback) {
        this.bytes += chunk.length || 0;
        cb(null, chunk);
      }
    }

    const counter = new CountingStream();

    // Create readable stream from the complete buffer and pipe through counter
    const fileStream = Readable.from([fullBuffer]);
    fileStream.pipe(counter);

    const storage = getStorageService();
    const bucket = 'documents';
    const version = 'v1';
    const unique = cryptoRandom();
    const key = `documents/${userId}/${doc.vehicleId}/${doc.id}/${version}/${unique}.${ext}`;

    await storage.putObject(bucket, key, counter, contentType, { 'x-original-filename': originalName });

    const updated = await this.service['repo'].updateStorageMeta(doc.id, userId, {
      storageBucket: bucket,
      storageKey: key,
      fileName: originalName,
      contentType: contentType,
      fileSize: counter.bytes,
      fileHash: null,
    });

    logger.info('Document upload completed', {
      operation: 'documents.upload.success',
      userId,
      documentId,
      vehicleId: doc.vehicleId,
      fileName: originalName,
      contentType,
      detectedType: detectedType.mime,
      fileSize: counter.bytes,
      storageKey: key,
    });

    return reply.code(200).send(updated);
  }

  async download(request: FastifyRequest<{ Params: IdParams }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const documentId = request.params.id;

    logger.info('Document download requested', {
      operation: 'documents.download',
      userId,
      documentId,
    });

    const doc = await this.service.getDocument(userId, documentId);
    if (!doc || !doc.storageBucket || !doc.storageKey) {
      logger.warn('Document or file not found for download', {
        operation: 'documents.download.not_found',
        userId,
        documentId,
        hasDocument: !!doc,
        hasStorageInfo: !!(doc?.storageBucket && doc?.storageKey),
      });
      return reply.code(404).send({ error: 'Not Found' });
    }

    const storage = getStorageService();
    let head: Partial<import('../../../core/storage/storage.service').HeadObjectResult> = {};
    try {
      head = await storage.headObject(doc.storageBucket, doc.storageKey);
    } catch { /* ignore */ }
    const contentType = head.contentType || doc.contentType || 'application/octet-stream';
    const filename = doc.fileName || path.basename(doc.storageKey);
    const inlineTypes = new Set(['application/pdf', 'image/jpeg', 'image/png']);
    const disposition = inlineTypes.has(contentType) ? 'inline' : 'attachment';

    reply.header('Content-Type', contentType);
    reply.header('Content-Disposition', `${disposition}; filename="${encodeURIComponent(filename)}"`);

    logger.info('Document download initiated', {
      operation: 'documents.download.success',
      userId,
      documentId,
      vehicleId: doc.vehicleId,
      fileName: filename,
      contentType,
      disposition,
      fileSize: head.size || doc.fileSize,
    });

    const stream = await storage.getObjectStream(doc.storageBucket, doc.storageKey);
    return reply.send(stream);
  }

  async listByVehicle(request: FastifyRequest<{ Params: { vehicleId: string } }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const vehicleId = request.params.vehicleId;

    logger.info('Documents by vehicle requested', {
      operation: 'documents.listByVehicle',
      userId,
      vehicleId,
    });

    try {
      const docs = await this.service.getDocumentsByVehicle(userId, vehicleId);

      logger.info('Documents by vehicle retrieved', {
        operation: 'documents.listByVehicle.success',
        userId,
        vehicleId,
        documentCount: docs.length,
      });

      return reply.code(200).send(docs);
    } catch (e: any) {
      if (e.statusCode === 403) {
        logger.warn('Vehicle not found or not owned', {
          operation: 'documents.listByVehicle.forbidden',
          userId,
          vehicleId,
        });
        return reply.code(403).send({ error: 'Forbidden', message: e.message });
      }
      throw e;
    }
  }

  async addVehicle(request: FastifyRequest<{ Params: { id: string; vehicleId: string } }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const { id: documentId, vehicleId } = request.params;

    logger.info('Add vehicle to document requested', {
      operation: 'documents.addVehicle',
      userId,
      documentId,
      vehicleId,
    });

    try {
      const updated = await this.service.addVehicleToDocument(userId, documentId, vehicleId);

      if (!updated) {
        logger.warn('Document not updated (possibly duplicate vehicle)', {
          operation: 'documents.addVehicle.not_updated',
          userId,
          documentId,
          vehicleId,
        });
        return reply.code(400).send({ error: 'Bad Request', message: 'Vehicle could not be added' });
      }

      logger.info('Vehicle added to document', {
        operation: 'documents.addVehicle.success',
        userId,
        documentId,
        vehicleId,
        sharedVehicleCount: updated.sharedVehicleIds.length,
      });

      return reply.code(200).send(updated);
    } catch (e: any) {
      if (e.statusCode === 404) {
        logger.warn('Document not found for adding vehicle', {
          operation: 'documents.addVehicle.not_found',
          userId,
          documentId,
          vehicleId,
        });
        return reply.code(404).send({ error: 'Not Found', message: e.message });
      }
      if (e.statusCode === 400) {
        logger.warn('Bad request for adding vehicle', {
          operation: 'documents.addVehicle.bad_request',
          userId,
          documentId,
          vehicleId,
          reason: e.message,
        });
        return reply.code(400).send({ error: 'Bad Request', message: e.message });
      }
      if (e.statusCode === 403) {
        logger.warn('Forbidden - vehicle not owned', {
          operation: 'documents.addVehicle.forbidden',
          userId,
          documentId,
          vehicleId,
        });
        return reply.code(403).send({ error: 'Forbidden', message: e.message });
      }
      throw e;
    }
  }

  async removeVehicle(request: FastifyRequest<{ Params: { id: string; vehicleId: string } }>, reply: FastifyReply) {
    const userId = (request as any).user?.sub as string;
    const { id: documentId, vehicleId } = request.params;

    logger.info('Remove vehicle from document requested', {
      operation: 'documents.removeVehicle',
      userId,
      documentId,
      vehicleId,
    });

    try {
      const updated = await this.service.removeVehicleFromDocument(userId, documentId, vehicleId);

      if (!updated) {
        // Document was soft deleted
        logger.info('Document soft deleted (primary vehicle removed, no shared vehicles)', {
          operation: 'documents.removeVehicle.deleted',
          userId,
          documentId,
          vehicleId,
        });
        return reply.code(204).send();
      }

      logger.info('Vehicle removed from document', {
        operation: 'documents.removeVehicle.success',
        userId,
        documentId,
        vehicleId,
        sharedVehicleCount: updated.sharedVehicleIds.length,
        primaryVehicleId: updated.vehicleId,
      });

      return reply.code(200).send(updated);
    } catch (e: any) {
      if (e.statusCode === 404) {
        logger.warn('Document not found for removing vehicle', {
          operation: 'documents.removeVehicle.not_found',
          userId,
          documentId,
          vehicleId,
        });
        return reply.code(404).send({ error: 'Not Found', message: e.message });
      }
      if (e.statusCode === 400) {
        logger.warn('Bad request for removing vehicle', {
          operation: 'documents.removeVehicle.bad_request',
          userId,
          documentId,
          vehicleId,
          reason: e.message,
        });
        return reply.code(400).send({ error: 'Bad Request', message: e.message });
      }
      throw e;
    }
  }
}

function cryptoRandom(): string {
  // Cryptographically secure random suffix for object keys
  return crypto.randomBytes(32).toString('hex');
}
