/**
 * UploadSessionManager
 *
 * Manages chunked upload sessions for large file uploads.
 * Handles session lifecycle, chunk tracking, and assembly.
 */

const fs = require('fs');
const path = require('path');
const { v4: uuid } = require('uuid');

const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours

/**
 * Ensure directory exists
 */
const ensureDirectoryExistence = (filePath) => {
  const dirname = path.dirname(filePath);

  if (fs.existsSync(dirname)) {
    return true;
  }

  ensureDirectoryExistence(dirname);
  fs.mkdirSync(dirname);
};

/**
 * Append one file to another
 */
const streamAppendFile = async (targetPath, sourcePath) => {
  await new Promise((resolve, reject) => {
    const writeStream = fs.createWriteStream(targetPath, { flags: 'a' });
    const readStream = fs.createReadStream(sourcePath);

    writeStream.on('error', reject);
    readStream.on('error', reject);
    writeStream.on('finish', resolve);
    readStream.pipe(writeStream, { end: true });
  });
};

class UploadSessionManager {
  /**
   * @param {Object} options
   * @param {string} options.sessionDir - Base directory for upload sessions
   * @param {number} [options.ttlMs] - Session TTL in milliseconds
   */
  constructor(options = {}) {
    this.sessionDir = options.sessionDir;
    this.ttlMs = options.ttlMs || DEFAULT_TTL_MS;
  }

  /**
   * Get session directory path
   */
  getSessionDir(sessionId) {
    return path.join(this.sessionDir, sessionId);
  }

  /**
   * Get session metadata path
   */
  getMetaPath(sessionId) {
    return path.join(this.getSessionDir(sessionId), 'meta.json');
  }

  /**
   * Get session chunks directory
   */
  getChunksDir(sessionId) {
    return path.join(this.getSessionDir(sessionId), 'chunks');
  }

  /**
   * Get chunk file path
   */
  getChunkPath(sessionId, chunkIndex) {
    return path.join(this.getChunksDir(sessionId), `${String(chunkIndex)}.part`);
  }

  /**
   * Read session metadata
   * @returns {Object|null}
   */
  readMeta(sessionId) {
    const metaPath = this.getMetaPath(sessionId);

    if (!fs.existsSync(metaPath)) {
      return null;
    }

    const raw = fs.readFileSync(metaPath, 'utf8');
    return JSON.parse(raw);
  }

  /**
   * Write session metadata
   */
  writeMeta(sessionId, payload) {
    const metaPath = this.getMetaPath(sessionId);
    ensureDirectoryExistence(metaPath);
    fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), 'utf8');
  }

  /**
   * Create a new upload session
   * @param {Object} options
   * @param {string} options.filename - Original filename
   * @param {string} options.folder - Target folder
   * @param {number} options.totalChunks - Total number of chunks
   * @param {number} [options.totalSize] - Total file size
   * @param {string} [options.userId] - User ID
   * @param {string} [options.contentType] - File content type
   * @returns {string} Session ID
   */
  createSession(options) {
    const sessionId = uuid();
    const chunksDir = this.getChunksDir(sessionId);

    ensureDirectoryExistence(chunksDir);
    fs.mkdirSync(chunksDir, { recursive: true });

    const now = new Date().toISOString();
    const meta = {
      sessionId,
      filename: options.filename,
      folder: options.folder,
      totalChunks: options.totalChunks,
      totalSize: options.totalSize || 0,
      userId: options.userId || null,
      contentType: options.contentType || null,
      uploadedChunks: {},
      createdAt: now,
      updatedAt: now,
    };

    this.writeMeta(sessionId, meta);
    return sessionId;
  }

  /**
   * Save a chunk
   * @param {string} sessionId
   * @param {number} chunkIndex
   * @param {Buffer} data
   */
  async saveChunk(sessionId, chunkIndex, data) {
    const chunkPath = this.getChunkPath(sessionId, chunkIndex);
    ensureDirectoryExistence(chunkPath);
    fs.writeFileSync(chunkPath, data);

    const meta = this.readMeta(sessionId);
    if (meta) {
      meta.uploadedChunks[chunkIndex] = {
        size: data.length,
        uploadedAt: new Date().toISOString(),
      };
      meta.updatedAt = new Date().toISOString();
      this.writeMeta(sessionId, meta);
    }
  }

  /**
   * Check if a chunk exists
   */
  chunkExists(sessionId, chunkIndex) {
    const chunkPath = this.getChunkPath(sessionId, chunkIndex);
    return fs.existsSync(chunkPath);
  }

  /**
   * Check if session is complete
   */
  isComplete(sessionId) {
    const meta = this.readMeta(sessionId);
    if (!meta) return false;

    const uploadedCount = Object.keys(meta.uploadedChunks).length;
    return uploadedCount >= meta.totalChunks;
  }

  /**
   * Assemble chunks into final file
   * @param {string} sessionId
   * @param {string} targetPath - Path for assembled file
   */
  async assembleChunks(sessionId, targetPath) {
    const meta = this.readMeta(sessionId);
    if (!meta) {
      throw new Error('Session not found');
    }

    ensureDirectoryExistence(targetPath);

    // Create empty target file
    fs.writeFileSync(targetPath, '');

    // Append chunks in order
    for (let i = 0; i < meta.totalChunks; i++) {
      const chunkPath = this.getChunkPath(sessionId, i);
      if (!fs.existsSync(chunkPath)) {
        throw new Error(`Missing chunk ${i}`);
      }
      await streamAppendFile(targetPath, chunkPath);
    }

    return targetPath;
  }

  /**
   * Remove an upload session
   */
  removeSession(sessionId) {
    const sessionDir = this.getSessionDir(sessionId);

    if (fs.existsSync(sessionDir)) {
      fs.rmSync(sessionDir, { recursive: true, force: true });
    }
  }

  /**
   * Cleanup expired sessions
   */
  cleanupExpiredSessions() {
    if (!fs.existsSync(this.sessionDir)) {
      return;
    }

    const now = Date.now();
    const sessionIds = fs.readdirSync(this.sessionDir);

    sessionIds.forEach((sessionId) => {
      try {
        const meta = this.readMeta(sessionId);
        if (!meta) {
          this.removeSession(sessionId);
          return;
        }

        const updatedAt = new Date(meta.updatedAt || meta.createdAt || 0).getTime();
        if (!updatedAt || now - updatedAt > this.ttlMs) {
          this.removeSession(sessionId);
        }
      } catch (error) {
        this.removeSession(sessionId);
      }
    });
  }
}

module.exports = UploadSessionManager;
