const crypto = require("crypto");
const fs = require("fs");
const fsp = require("fs/promises");
const os = require("os");
const multer = require("multer");
const { minioClient, elasticsearchClient } = require("../config/clients");
const { metadataIndex, storageBucket, maxUploadBytes, streamThresholdBytes } = require("../config/env");
const {
  sanitizeObjectName,
  normalizeString,
  parseTags,
  normalizeFolderPath,
  parseOptionalDate,
  normalizeSortField,
  normalizeSortDirection,
  buildRecursiveScopeQuery,
  buildDirectChildrenScopeQuery,
  parentPathOf,
  sanitizeFolderName,
  buildFolderPath
} = require("../utils/helpers");

const serviceStatus = {
  elasticsearch: "unknown",
  minio: "unknown",
  initializedAt: null
};

let minioBucketReady = false;
let elasticIndexReady = false;

const upload = multer({
  storage: multer.diskStorage({
    destination: (_req, _file, cb) => cb(null, os.tmpdir()),
    filename: (_req, file, cb) => {
      const unique = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString("hex");
      cb(null, `${unique}-${sanitizeObjectName(file.originalname || "upload.bin")}`);
    }
  }),
  limits: { fileSize: maxUploadBytes }
});

async function initializeDependencies() {
  await Promise.allSettled([ensureElasticsearchIndex(), ensureMinioBucket()]);
  serviceStatus.initializedAt = new Date().toISOString();
}

async function ensureStorageDependencies() {
  await Promise.all([ensureElasticsearchIndex(), ensureMinioBucket()]);
}

async function ensureMinioBucket() {
  if (minioBucketReady) {
    return;
  }

  const exists = await minioClient.bucketExists(storageBucket);
  if (!exists) {
    await minioClient.makeBucket(storageBucket);
  }

  minioBucketReady = true;
  serviceStatus.minio = "ok";
}

async function ensureElasticsearchIndex() {
  if (elasticIndexReady) {
    return;
  }

  const existsResponse = await elasticsearchClient.indices.exists({ index: metadataIndex });
  const exists = typeof existsResponse === "boolean" ? existsResponse : Boolean(existsResponse && existsResponse.body);
  if (!exists) {
    await elasticsearchClient.indices.create({
      index: metadataIndex,
      mappings: {
        properties: {
          userId: { type: "keyword" },
          username: { type: "keyword" },
          entryType: { type: "keyword" },
          displayName: { type: "text", fields: { keyword: { type: "keyword", ignore_above: 256 } } },
          originalFileName: { type: "text", fields: { keyword: { type: "keyword", ignore_above: 256 } } },
          folderPath: { type: "keyword" },
          parentFolderPath: { type: "keyword" },
          objectName: { type: "keyword" },
          bucket: { type: "keyword" },
          contentType: { type: "keyword" },
          size: { type: "long" },
          tags: { type: "keyword" },
          tagsText: { type: "text" },
          clientLastModifiedAt: { type: "date" },
          serverTempCreatedAt: { type: "date" },
          serverTempModifiedAt: { type: "date" },
          uploadedAt: { type: "date" }
        }
      }
    });
  }

  elasticIndexReady = true;
  serviceStatus.elasticsearch = "ok";
}

async function checkHealth() {
  const checks = await Promise.allSettled([checkElasticsearch(), checkMinio()]);
  serviceStatus.elasticsearch = checks[0].status === "fulfilled" ? "ok" : "error";
  serviceStatus.minio = checks[1].status === "fulfilled" ? "ok" : "error";

  return {
    status: serviceStatus.elasticsearch === "ok" && serviceStatus.minio === "ok" ? "ok" : "degraded",
    service: "search-first-dis-storage",
    timestamp: new Date().toISOString(),
    dependencies: {
      elasticsearch: serviceStatus.elasticsearch,
      minio: serviceStatus.minio
    }
  };
}

async function listFilesForUser(user, query) {
  const queryText = normalizeString(query.query);
  const tagFilter = normalizeString(query.tag);
  const folderPath = normalizeFolderPath(query.folderPath);
  const createdFrom = parseOptionalDate(query.createdFrom);
  const createdTo = parseOptionalDate(query.createdTo);
  const sortBy = normalizeSortField(query.sortBy);
  const sortDir = normalizeSortDirection(query.sortDir);
  const hasSearchFilters = Boolean(queryText || tagFilter || createdFrom || createdTo);

  const must = [{ term: { userId: user.id } }];
  must.push(hasSearchFilters ? buildRecursiveScopeQuery(folderPath) : buildDirectChildrenScopeQuery(folderPath));

  if (tagFilter) {
    must.push({ term: { tags: tagFilter } });
  }
  if (queryText) {
    must.push({
      bool: {
        should: [{ match: { displayName: queryText } }, { match: { originalFileName: queryText } }, { match: { tagsText: queryText } }],
        minimum_should_match: 1
      }
    });
  }
  if (createdFrom || createdTo) {
    const range = {};
    if (createdFrom) {
      range.gte = createdFrom;
    }
    if (createdTo) {
      range.lte = createdTo;
    }
    must.push({ range: { clientLastModifiedAt: range } });
  }

  const result = await elasticsearchClient.search({
    index: metadataIndex,
    size: 100,
    sort: [{ [sortBy]: { order: sortDir } }, { uploadedAt: { order: "desc" } }],
    query: { bool: { must } }
  });

  return (result.hits.hits || []).map((hit) => ({ id: hit._id, ...hit._source }));
}

async function createFileEntry(user, file, body) {
  const displayNameInput = normalizeString(body.filename);
  const displayName = displayNameInput || file.originalname;
  const tags = parseTags(body.tags);
  const folderPath = normalizeFolderPath(body.folderPath);
  const clientLastModifiedAt = parseOptionalDate(body.clientLastModifiedAt);
  const objectId = crypto.randomUUID ? crypto.randomUUID() : crypto.randomBytes(16).toString("hex");
  const folderPrefix = folderPath ? `${folderPath.replace(/^\//, "")}/` : "";
  const objectName = `${user.id}/${folderPrefix}${objectId}-${sanitizeObjectName(file.originalname)}`;

  const localFileStats = await fsp.stat(file.path);
  await uploadTempFileToMinio(file, objectName);

  const metadata = {
    entryType: "file",
    userId: user.id,
    username: user.username,
    displayName,
    originalFileName: file.originalname,
    folderPath,
    parentFolderPath: parentPathOf(folderPath),
    objectName,
    bucket: storageBucket,
    contentType: file.mimetype || "application/octet-stream",
    size: file.size,
    tags,
    tagsText: tags.join(" "),
    clientLastModifiedAt,
    serverTempCreatedAt: localFileStats.birthtime ? localFileStats.birthtime.toISOString() : null,
    serverTempModifiedAt: localFileStats.mtime ? localFileStats.mtime.toISOString() : null,
    uploadedAt: new Date().toISOString()
  };

  const indexResponse = await elasticsearchClient.index({
    index: metadataIndex,
    document: metadata,
    refresh: "wait_for"
  });

  return { id: indexResponse._id, ...metadata };
}

async function createFolderEntry(user, body) {
  const name = sanitizeFolderName(body && body.name);
  const parentFolderPath = normalizeFolderPath(body && body.parentFolderPath);
  const tags = parseTags(body && body.tags);

  if (!name) {
    return { error: "folder name is required", status: 400 };
  }

  const folderPath = buildFolderPath(parentFolderPath, name);
  const duplicateCheck = await elasticsearchClient.search({
    index: metadataIndex,
    size: 1,
    query: {
      bool: {
        must: [{ term: { userId: user.id } }, { term: { entryType: "folder" } }, { term: { folderPath } }]
      }
    }
  });

  if ((duplicateCheck.hits.hits || []).length > 0) {
    return { error: "folder already exists", status: 409 };
  }

  const metadata = {
    entryType: "folder",
    userId: user.id,
    username: user.username,
    displayName: name,
    originalFileName: name,
    folderPath,
    parentFolderPath,
    objectName: "",
    bucket: storageBucket,
    contentType: "application/x-directory",
    size: 0,
    tags,
    tagsText: tags.join(" "),
    uploadedAt: new Date().toISOString()
  };

  const indexResponse = await elasticsearchClient.index({
    index: metadataIndex,
    document: metadata,
    refresh: "wait_for"
  });
  return { data: { id: indexResponse._id, ...metadata }, status: 201 };
}

async function loadOwnedEntry(documentId, userId) {
  let source;
  try {
    const result = await elasticsearchClient.get({ index: metadataIndex, id: documentId });
    source = result._source;
  } catch (error) {
    if (error && error.meta && error.meta.statusCode === 404) {
      const notFoundError = new Error("Not found");
      notFoundError.code = "NOT_FOUND";
      throw notFoundError;
    }
    throw error;
  }

  if (!source || source.userId !== userId) {
    const notFoundError = new Error("Not found");
    notFoundError.code = "NOT_FOUND";
    throw notFoundError;
  }
  return source;
}

async function deleteEntryById(id, userId, force) {
  const entry = await loadOwnedEntry(id, userId);
  if (entry.entryType === "folder") {
    const folderPath = normalizeFolderPath(entry.folderPath);
    const descendants = await listFolderDescendants(userId, folderPath);
    const fileCount = descendants.filter((item) => item.entryType === "file").length;
    const itemCount = descendants.length;

    if (fileCount > 0 && !force) {
      return { error: "folder contains files", status: 409, requiresConfirmation: true, fileCount, itemCount };
    }

    await deleteEntriesAndObjects(descendants);
    await elasticsearchClient.delete({ index: metadataIndex, id, refresh: "wait_for" });
    return { deleted: { id, entryType: "folder", descendantFileCount: fileCount, descendantItemCount: itemCount } };
  }

  await deleteMinioObjectIfPresent(entry);
  await elasticsearchClient.delete({ index: metadataIndex, id, refresh: "wait_for" });
  return { deleted: { id, entryType: "file" } };
}

async function getDownloadStream(id, userId) {
  const fileRecord = await loadOwnedEntry(id, userId);
  if (fileRecord.entryType === "folder") {
    return { error: "folders cannot be downloaded", status: 400 };
  }

  const stream = await minioClient.getObject(fileRecord.bucket || storageBucket, fileRecord.objectName);
  return { fileRecord, stream };
}

async function listFolderDescendants(userId, folderPath) {
  const normalizedFolderPath = normalizeFolderPath(folderPath);
  const prefix = normalizedFolderPath ? `${normalizedFolderPath}/` : "/";

  const result = await elasticsearchClient.search({
    index: metadataIndex,
    size: 1000,
    query: {
      bool: {
        must: [
          { term: { userId } },
          {
            bool: {
              should: [
                { term: { folderPath: normalizedFolderPath } },
                { term: { parentFolderPath: normalizedFolderPath } },
                { prefix: { folderPath: prefix } }
              ],
              minimum_should_match: 1
            }
          }
        ]
      }
    }
  });

  return (result.hits.hits || [])
    .map((hit) => ({ id: hit._id, ...hit._source }))
    .filter((entry) => !(entry.entryType === "folder" && normalizeFolderPath(entry.folderPath) === normalizedFolderPath));
}

async function deleteEntriesAndObjects(entries) {
  const files = entries.filter((entry) => entry.entryType === "file");
  for (const file of files) {
    await deleteMinioObjectIfPresent(file);
  }
  if (!entries.length) {
    return;
  }

  const operations = [];
  for (const entry of entries) {
    operations.push({ delete: { _index: metadataIndex, _id: entry.id } });
  }

  await elasticsearchClient.bulk({ refresh: "wait_for", operations });
}

async function deleteMinioObjectIfPresent(entry) {
  if (!entry || entry.entryType !== "file" || !entry.objectName) {
    return;
  }

  try {
    await minioClient.removeObject(entry.bucket || storageBucket, entry.objectName);
  } catch (error) {
    if (error && (error.code === "NoSuchKey" || error.code === "NotFound")) {
      return;
    }
    throw error;
  }
}

async function checkElasticsearch() {
  await elasticsearchClient.ping();
}

async function checkMinio() {
  await minioClient.bucketExists(storageBucket).catch(async (error) => {
    if (error) {
      throw error;
    }
  });
}

async function uploadTempFileToMinio(file, objectName) {
  const contentType = file.mimetype || "application/octet-stream";
  if (file.size <= streamThresholdBytes) {
    const buffer = await fsp.readFile(file.path);
    await minioClient.putObject(storageBucket, objectName, buffer, file.size, {
      "Content-Type": contentType
    });
    return;
  }

  const readStream = fs.createReadStream(file.path);
  try {
    await minioClient.putObject(storageBucket, objectName, readStream, file.size, {
      "Content-Type": contentType
    });
  } finally {
    readStream.destroy();
  }
}

module.exports = {
  uploadSingleFile: upload.single("file"),
  initializeDependencies,
  ensureStorageDependencies,
  checkHealth,
  listFilesForUser,
  createFileEntry,
  createFolderEntry,
  getDownloadStream,
  deleteEntryById
};
