const db = require('../../database/database');
const AdminRepository = require('../../repositories/admin/AdminRepository');
const { s3 } = require('../../utils/exoscaleUploader');
const { ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand, PutObjectCommand, HeadObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const { logger } = require('../../middleware/logger');
const path = require('path');
const { Readable } = require('stream');
const { ZipArchive } = require('archiver');

async function executeDump(sql) {
  const conn = await db.getMultiStatementConnection();
  try {
    const [rows, fields] = await conn.query(sql);
    return { rows, fields };
  } finally {
    try { await conn.end(); } catch (_) {}
  }
}

async function listAllKeys(prefix) {
  const keys = [];
  let token = undefined;
  do {
    const res = await s3.send(new ListObjectsV2Command({
      Bucket: process.env.EXOSCALE_BUCKET,
      Prefix: prefix,
      ContinuationToken: token
    }));
    const batch = (res && res.Contents ? res.Contents : [])
      .map(item => item && item.Key)
      .filter(Boolean);
    keys.push(...batch);
    token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
  } while (token);
  return keys;
}

async function listTopLevelFolders(prefix) {
  const folders = [];
  let token = undefined;
  do {
    const res = await s3.send(new ListObjectsV2Command({
      Bucket: process.env.EXOSCALE_BUCKET,
      Prefix: prefix,
      Delimiter: '/',
      ContinuationToken: token
    }));
    const batch = (res && res.CommonPrefixes ? res.CommonPrefixes : [])
      .map(item => item && item.Prefix)
      .filter(Boolean);
    folders.push(...batch);
    token = res && res.IsTruncated ? res.NextContinuationToken : undefined;
  } while (token);
  return folders;
}

function toReadable(body) {
  if (!body) return null;
  if (typeof body.pipe === 'function') return body;
  if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
    return Readable.from(body);
  }
  return null;
}

function getTimestamp() {
  const d = new Date();
  const pad = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
}

async function objectExists(key) {
  try {
    await s3.send(new HeadObjectCommand({
      Bucket: process.env.EXOSCALE_BUCKET,
      Key: key
    }));
    return true;
  } catch (e) {
    const status = e && (e.$metadata && e.$metadata.httpStatusCode);
    if (status === 404) return false;
    return false;
  }
}

function appendTimestampToKey(key) {
  const ext = path.posix.extname(key);
  const base = ext ? key.slice(0, -ext.length) : key;
  return `${base}-${getTimestamp()}${ext}`;
}

async function getUsersForScan() {
  const conn = await db.getConnection();
  try {
    return await AdminRepository.getUserList(conn);
  } finally {
    try { conn.release(); } catch (_) {}
  }
}

function getDisplayName(user) {
  if (!user) return null;
  if ((user.user_type || '').toLowerCase() === 'company') return user.company_name || null;
  const first = user.first_name || '';
  const last = user.last_name || '';
  const full = `${first} ${last}`.trim();
  return full || null;
}

async function listFolderStructureIssues() {
  const users = await getUsersForScan();
  const results = [];
  let scannedUsers = 0;

  for (const user of users) {
    const userType = (user.user_type || '').toLowerCase();
    if (!['personal', 'company'].includes(userType)) continue;
    scannedUsers += 1;

    const contractCategory = userType === 'company' ? 'company' : 'personal';
    const basePrefix = `contracts/${contractCategory}/${user.id}/`;
    const keys = await listAllKeys(basePrefix);
    if (!keys.length) continue;

    const hasContractFolder = keys.some(k => k.startsWith(`${basePrefix}contract/`));
    const hasGdprFolder = keys.some(k => k.startsWith(`${basePrefix}gdpr/`));
    if (hasContractFolder && hasGdprFolder) continue;

    results.push({
      userId: user.id,
      email: user.email,
      userType,
      name: getDisplayName(user),
      contractCategory,
      basePrefix,
      totalObjects: keys.length,
      hasContractFolder,
      hasGdprFolder
    });
  }

  return {
    list: results,
    meta: {
      scannedUsers,
      invalidCount: results.length
    }
  };
}

async function createFolderStructure({ userId } = {}) {
  const { list } = await listFolderStructureIssues();
  const targetId = userId ? Number(userId) : null;
  const targets = targetId ? list.filter(u => u.userId === targetId) : list;
  const results = [];
  let createdTotal = 0;
  let errorCount = 0;

  for (const target of targets) {
    const prefix = target.basePrefix;
    const keys = await listAllKeys(prefix);
    const hasContractFolder = keys.some(k => k.startsWith(`${prefix}contract/`));
    const hasGdprFolder = keys.some(k => k.startsWith(`${prefix}gdpr/`));

    let created = 0;
    const errors = [];

    const ensureFolder = async (folderKey) => {
      try {
        await s3.send(new PutObjectCommand({
          Bucket: process.env.EXOSCALE_BUCKET,
          Key: folderKey,
          Body: ''
        }));
        created += 1;
      } catch (e) {
        errorCount += 1;
        const message = e && (e.message || e.toString());
        logger.error('DevManagementService.createFolderStructure:create_failed', { folderKey, error: message });
        errors.push({ folderKey, message });
      }
    };

    if (!hasContractFolder) await ensureFolder(`${prefix}contract/`);
    if (!hasGdprFolder) await ensureFolder(`${prefix}gdpr/`);

    createdTotal += created;
    results.push({
      userId: target.userId,
      contractCategory: target.contractCategory,
      created,
      errors
    });
  }

  return {
    results,
    meta: {
      processedUsers: targets.length,
      createdTotal,
      errorCount
    }
  };
}

async function listLooseFiles() {
  const users = await getUsersForScan();
  const results = [];
  let scannedUsers = 0;

  for (const user of users) {
    const userType = (user.user_type || '').toLowerCase();
    if (!['personal', 'company'].includes(userType)) continue;
    scannedUsers += 1;

    const contractCategory = userType === 'company' ? 'company' : 'personal';
    const basePrefix = `contracts/${contractCategory}/${user.id}/`;
    const keys = await listAllKeys(basePrefix);
    if (!keys.length) continue;

    const looseKeys = keys
      .filter(k => !!k)
      .filter(k => !k.endsWith('/'))
      .filter(k => !k.startsWith(`${basePrefix}contract/`))
      .filter(k => !k.startsWith(`${basePrefix}gdpr/`));

    if (!looseKeys.length) continue;

    results.push({
      userId: user.id,
      email: user.email,
      userType,
      name: getDisplayName(user),
      contractCategory,
      basePrefix,
      looseObjects: looseKeys.length,
      sampleKeys: looseKeys.slice(0, 5)
    });
  }

  return {
    list: results,
    meta: {
      scannedUsers,
      looseCount: results.length
    }
  };
}

async function moveLooseFilesToContract({ userId } = {}) {
  const { list } = await listLooseFiles();
  const targetId = userId ? Number(userId) : null;
  const targets = targetId ? list.filter(u => u.userId === targetId) : list;
  const results = [];
  let movedTotal = 0;
  let errorCount = 0;

  for (const target of targets) {
    const prefix = target.basePrefix;
    const keys = await listAllKeys(prefix);
    const toMove = keys
      .filter(k => !!k)
      .filter(k => !k.endsWith('/'))
      .filter(k => !k.startsWith(`${prefix}contract/`))
      .filter(k => !k.startsWith(`${prefix}gdpr/`));

    let moved = 0;
    let skipped = 0;
    const errors = [];

    for (const key of toMove) {
      const baseName = path.posix.basename(key);
      let destKey = `${prefix}contract/${baseName}`;
      if (await objectExists(destKey)) {
        destKey = appendTimestampToKey(destKey);
      }
      if (destKey === key) {
        skipped += 1;
        continue;
      }
      try {
        const copySource = `${process.env.EXOSCALE_BUCKET}/${encodeURIComponent(key)}`;
        await s3.send(new CopyObjectCommand({
          Bucket: process.env.EXOSCALE_BUCKET,
          CopySource: copySource,
          Key: destKey
        }));
        await s3.send(new DeleteObjectCommand({
          Bucket: process.env.EXOSCALE_BUCKET,
          Key: key
        }));
        moved += 1;
      } catch (e) {
        errorCount += 1;
        const message = e && (e.message || e.toString());
        logger.error('DevManagementService.moveLooseFilesToContract:move_failed', { key, destKey, error: message });
        errors.push({ key, destKey, message });
      }
    }

    movedTotal += moved;
    results.push({
      userId: target.userId,
      contractCategory: target.contractCategory,
      moved,
      skipped,
      errors
    });
  }

  return {
    results,
    meta: {
      processedUsers: targets.length,
      movedTotal,
      errorCount
    }
  };
}

async function listGhostDirectories() {
  const users = await getUsersForScan();
  const userIds = new Set(users.map(u => Number(u.id)).filter(n => Number.isFinite(n)));
  const results = [];

  const categories = ['personal', 'company'];
  for (const category of categories) {
    const base = `contracts/${category}/`;
    const prefixes = await listTopLevelFolders(base);
    for (const prefix of prefixes) {
      const idPart = prefix.replace(base, '').replace(/\/$/, '');
      if (!/^[0-9]+$/.test(idPart)) continue;
      const uid = Number(idPart);
      if (!userIds.has(uid)) {
        results.push({
          userId: uid,
          contractCategory: category,
          basePrefix: prefix
        });
      }
    }
  }

  return {
    list: results,
    meta: {
      ghostCount: results.length
    }
  };
}

async function streamStorageArchive(outputStream) {
  const archive = new ZipArchive({ zlib: { level: 9 } });

  archive.on('warning', (error) => {
    logger.warn('DevManagementService.streamStorageArchive:warning', { error: error?.message || String(error) });
  });

  archive.on('error', (error) => {
    logger.error('DevManagementService.streamStorageArchive:archive_error', { error: error?.message || String(error) });
    outputStream.destroy(error);
  });

  archive.pipe(outputStream);

  let totalFiles = 0;
  let totalBytes = 0;
  let token = undefined;

  do {
    const page = await s3.send(new ListObjectsV2Command({
      Bucket: process.env.EXOSCALE_BUCKET,
      ContinuationToken: token
    }));

    const objects = (page && page.Contents ? page.Contents : [])
      .filter(item => item && item.Key)
      .filter(item => !item.Key.endsWith('/'));

    for (const object of objects) {
      const key = object.Key;
      const response = await s3.send(new GetObjectCommand({
        Bucket: process.env.EXOSCALE_BUCKET,
        Key: key
      }));
      const bodyStream = toReadable(response && response.Body);

      if (!bodyStream) {
        throw new Error(`Object body for ${key} is not readable`);
      }

      await new Promise((resolve, reject) => {
        bodyStream.once('error', reject);
        bodyStream.once('end', resolve);
        archive.append(bodyStream, {
          name: key,
          date: object.LastModified || new Date()
        });
      });

      totalFiles += 1;
      totalBytes += Number(object.Size || 0);
    }

    token = page && page.IsTruncated ? page.NextContinuationToken : undefined;
  } while (token);

  await archive.finalize();

  return {
    totalFiles,
    totalBytes
  };
}

module.exports = {
  executeDump,
  listFolderStructureIssues,
  createFolderStructure,
  listLooseFiles,
  moveLooseFilesToContract,
  listGhostDirectories,
  streamStorageArchive
};
