/**
 * Copyright Super iPaaS Integration LLC, an IBM Company 2024
 */
import { Metadata } from "@apic/api-model/common/Metadata.js";
import AdmZip from "adm-zip";
import fs from "fs";
import { AssetCache } from "../../cache/asset-cache.js";
import { COLON, COMMA } from "../../constants/app-constants.js";
import { AssetCacheModel } from "../../model/asset-cache-model.js";
import { BaseAsset } from "../../model/assets-model.js";
import { equalsIgnoreCase, isNullOrUndefined } from "../common/data-helper.js";
import {
  getRandomFileName,
  getSubDirectory,
  isDirectory,
  isDirOrFileExists,
  isYamlFile,
  normalizePath,
  readFile,
} from "../common/fs-helper.js";
import { showError, showInfo, showWarning } from "../common/message-helper.js";
import { readMultiYaml, readYaml } from "../common/yaml-helper.js";
import {
  checkForDependencyAssets,
  loadCacheWithProject,
} from "./asset-cache-helper.js";
import { getTargetModelAssetKind, isValidAsset } from "./asset-helper.js";
import { getOtherProjectsNames } from "./root-dir-helper.js";
import { DebugManager } from "../../debug/debug-manager.js";
import {
  ADDING_DEPENDENCY,
  ASSERT_ADDED,
  ASSET_DEPENDENCIES,
  DEPENDENT_ASSETS_TO_BE_PROCESSED,
  DUPLICATE_ENTRIES_FOR_KIND,
  FOLLOWING,
  INSIDE_THE_PROJECT_PATH,
  INVALID_DIRECTORY,
  IS_FOUND_IN,
  KIND,
  METADATA_NAME,
  NAME,
  NO_ENTRIES_FOUND_FOR_KIND,
  NO_FURTHER_DEPENDENCY,
  REF,
  SEARCHING,
  THERE_ARE,
  VERSION,
} from "../../constants/message-constants.js";
import { KindEnums } from "@apic/api-model/common/StudioEnums.js";
import { bundleApiDependency } from './api-build-helper.js';

const addDependencyAsset = (
  file: fs.Dirent,
  zip: AdmZip,
  fileExtension = ".yml"
) => {
  if (isNullOrUndefined(file)) {
    return;
  }
  const fileName = getRandomFileName(fileExtension);
  const filePath = normalizePath(`${file.parentPath}/${file.name}`);
  zip.addLocalFile(filePath, "dependencies", fileName);
};

const hasAssetInGivenAssets = (
  assets: BaseAsset[],
  metadataToSearch: Metadata,
  kindToSearch: string
) => {
  for (const asset of assets) {
    if (!isValidAsset(asset)) {
      continue;
    }

    if (
      isSameAsset(asset.metadata, metadataToSearch) &&
      equalsIgnoreCase(kindToSearch, getTargetModelAssetKind(asset.kind))
    ) {
      return true;
    }
  }
  return false;
};

// search for the dependency asset for the given asset ref value and project directory path
const searchAsset = (
  kindToSearch: string,
  assetRefValueToSearch: string,
  projectDirPath: string
): fs.Dirent | undefined => {
  if (!isDirOrFileExists(projectDirPath) || !isDirectory(projectDirPath)) {
    throw new Error(`${INVALID_DIRECTORY} ${projectDirPath}`);
  }
  const entries: fs.Dirent[] = fs.readdirSync(projectDirPath, {
    withFileTypes: true,
    recursive: true,
  });
  const metadataToSearch = fromAssetRefValue(assetRefValueToSearch);

  const filteredEntries = entries.filter((entry) => {
    if (entry.isDirectory()) {
      return false;
    }
    if (!isYamlFile(entry.name)) {
      return false;
    }

    const assets = readMultiYaml<BaseAsset>(
      normalizePath(`${entry.parentPath}/${entry.name}`),
      readFile(entry.parentPath, entry.name)
    );

    return hasAssetInGivenAssets(assets, metadataToSearch, kindToSearch);
  });

  if (
    filteredEntries.length > 1 &&
    DebugManager.getInstance().isDebugEnabled()
  ) {
    showWarning(
      `${DUPLICATE_ENTRIES_FOR_KIND} - '${kindToSearch}', ${METADATA_NAME} '${metadataToSearch.name}' ${IS_FOUND_IN} '${projectDirPath}'`
    );
  }
  if (
    filteredEntries.length === 0 &&
    DebugManager.getInstance().isDebugEnabled()
  ) {
    showWarning(
      `${NO_ENTRIES_FOUND_FOR_KIND} - '${kindToSearch}', ${METADATA_NAME} '${metadataToSearch.name}' ${IS_FOUND_IN} '${projectDirPath}'`
    );
    return undefined;
  }
  return filteredEntries[0];
};

const isSameAsset = (input1: Metadata, input2: Metadata): boolean => {
  const isNamespaceAndNameEqual =
    input1.namespace === input2.namespace && input1.name === input2.name;

  const isVersionEqual = (() => {
    const version1 = Number(input1.version);
    const version2 = Number(input2.version);

    if (Number.isNaN(version1) && Number.isNaN(version2)) {
      return input1.version === input2.version;
    }
    return version1 === version2;
  })();

  return isNamespaceAndNameEqual && isVersionEqual;
};

const fromAssetRefValue = (assetRefValue: string): Metadata => {
  const split = assetRefValue.split(COLON);
  if (split.length === 1) {
    return {
      name: split[0],
    };
  } else if (split.length === 2) {
    return {
      name: split[0],
      version: split[1],
    };
  }
  return {
    namespace: split[0],
    name: split[1],
    version: split[2],
  };
};

const searchAndBundleDependency = (
  cachedUnProcessedAsset: AssetCacheModel,
  rootDirPath: string,
  projects: Set<string>,
  zipFile: AdmZip
) => {
  try {
    for (const project of projects) {
      const projectDirPath = getSubDirectory(rootDirPath, project);
      if (DebugManager.getInstance().isDebugEnabled()) {
        showInfo(
          `\n\n ${SEARCHING}: ${KIND} - ${cachedUnProcessedAsset.kind} ${REF} -  ${cachedUnProcessedAsset.ref} ${INSIDE_THE_PROJECT_PATH} '${projectDirPath}'`
        );
      }
      const result = searchAsset(
        cachedUnProcessedAsset.kind,
        cachedUnProcessedAsset.ref,
        projectDirPath
      );

      if (!isNullOrUndefined(result)) {
        const fileContent = readFile(
          (result as fs.Dirent).parentPath,
          (result as fs.Dirent).name
        );
        const asset = readYaml<BaseAsset>(fileContent);
        const isApiKind = equalsIgnoreCase(
          getTargetModelAssetKind(asset.kind),
          KindEnums.API
        );
        /* (*) add dependency to zip file*/
        if (DebugManager.getInstance().isDebugEnabled()) {
          showInfo(
            `${ADDING_DEPENDENCY}: ${KIND}-'${asset.metadata.namespace}', ${NAME}-'${asset.metadata.name}', ${VERSION}-'${asset.metadata.version}'`
          );
        }

        if (isApiKind) {
          bundleApiDependency(asset, result as fs.Dirent, cachedUnProcessedAsset, rootDirPath, project, zipFile)
        } else {
          // Non-API dependencies go to dependencies folder (existing behavior)
          addDependencyAsset(result as fs.Dirent, zipFile);
          showInfo(
            `${ASSERT_ADDED} ${asset.metadata.namespace}:${asset.metadata.name}:${asset.metadata.version}`
          );
        }

        /* (*) mark the added asset as processed */
        AssetCache.getInstance().markAsProcessed(asset);
        /* (*) check for any further dependencies from the current asset */
        // Pass the source project to maintain the dependency chain
        const sourceProjectForNestedDeps = cachedUnProcessedAsset.sourceProject || project;
        checkForDependencyAssets(asset, sourceProjectForNestedDeps);

        return;
      }
    }
    /* (*) if there are no assets found, mark this unprocessed asset as checked. */
    AssetCache.getInstance().markUnProcessedAssetAsChecked(
      cachedUnProcessedAsset
    );
  } catch (error) {
    throw new Error(
      `Failure in search asset: kind - ${cachedUnProcessedAsset.kind} ref -  ${cachedUnProcessedAsset.ref
      } with error: ${(error as Error).message}`
    );
  }
};

const loadDependenciesFromProjects = (
  rootDirPath: string,
  projects: Set<string>,
  zipFile: AdmZip
) => {
  const newlyAddedUnProcessedAssets =
    AssetCache.getInstance().getNewlyAddedUnProcessedAssets();

  for (const newlyAddedUnProcessedAsset of newlyAddedUnProcessedAssets) {
    searchAndBundleDependency(
      newlyAddedUnProcessedAsset,
      rootDirPath,
      projects,
      zipFile
    );
  }
};

const checkAndLoadDependenciesFromProjects = (
  rootDirPath: string,
  projects: Set<string>,
  zipFile: AdmZip
) => {
  while (!haveCheckedUnProcessedAssets() && haveUnCheckedUnProcessedAssets()) {
    const unProcessedAssets =
      AssetCache.getInstance().getNewlyAddedUnProcessedAssets();
    if (DebugManager.getInstance().isDebugEnabled()) {
      showInfo(
        `\n\n ${THERE_ARE} ${unProcessedAssets.size} ${DEPENDENT_ASSETS_TO_BE_PROCESSED}`
      );

      // logging newly added dependencies
      showInfo(`${FOLLOWING} ${unProcessedAssets.size} ${ASSET_DEPENDENCIES} `);
    }
    unProcessedAssets.forEach((unProcessedAsset) => {
      if (DebugManager.getInstance().isDebugEnabled()) {
        showInfo(
          `${KIND}: '${unProcessedAsset.kind}' ${REF}: ${unProcessedAsset.ref}`
        );
      }
    });

    loadDependenciesFromProjects(rootDirPath, projects, zipFile);
  }
};

const checkCacheState = () => {
  if (haveCheckedUnProcessedAssets()) {
    const unProcessedAssets =
      AssetCache.getInstance().getCheckedUnProcessedAssets();
    showError(
      `Following  ${unProcessedAssets.size} asset dependencies cannot be resolved:`
    );
    unProcessedAssets.forEach((unProcessedAsset) =>
      showError(`kind: '${unProcessedAsset.kind}' ref: ${unProcessedAsset.ref}`)
    );
    throw new Error("Dependency assets cannot be resolved");
  }

  if (!haveUnCheckedUnProcessedAssets()) {
    const unProcessedAssets =
      AssetCache.getInstance().getNewlyAddedUnProcessedAssets();
    if (unProcessedAssets.size === 0) {
      if (DebugManager.getInstance().isDebugEnabled()) {
        showInfo(`${NO_FURTHER_DEPENDENCY}`);
      }
      return;
    }
  }
};

const haveCheckedUnProcessedAssets = () => {
  return AssetCache.getInstance().getCheckedUnProcessedAssets().size > 0;
};

const haveUnCheckedUnProcessedAssets = () => {
  return AssetCache.getInstance().getNewlyAddedUnProcessedAssets().size > 0;
};

const checkAndLoadDependencies = (
  rootDirPath: string,
  projectNames: string,
  zipFile: AdmZip,
  excludeCurrProj = false
) => {
  /* (*) Parse the current project assets and update cache with processed and to be processed */
  AssetCache.getInstance().clear();
  loadCacheWithProject(zipFile);

  /* (*) Check the current projects before checking in the other projects */
  if (!excludeCurrProj) {
    const currentProjects = new Set<string>(projectNames.split(COMMA));
    checkAndLoadDependenciesFromProjects(rootDirPath, currentProjects, zipFile);

    /* (*) mark unprocessed asset if any as unchecked to make to search in other projects */
    AssetCache.getInstance().markAllUnProcessedAssetAsUnchecked();
  }

  /* (*) Check and load refers to the dependencies directory */
  const otherProjects: Set<string> | null = getOtherProjectsNames(
    rootDirPath,
    projectNames
  );
  if (!otherProjects) {
    showInfo('Skip checking files in other projects as no other projects found');
    return;
  }
  checkAndLoadDependenciesFromProjects(rootDirPath, otherProjects, zipFile);

  /* (*) Check for cache state and throw error if some assets cannot be resolved*/
  checkCacheState();
};



export {
  addDependencyAsset,
  checkAndLoadDependencies,
  fromAssetRefValue,
  isSameAsset,
  searchAsset,
};
