import fsProm from 'node:fs/promises';
import path from 'node:path';

import {
  KNOWN_PACKAGES,
  rootPath,
  serviceKindOrders,
  VITEST_PKG_NAME,
} from '../config';
import type { Package, Workspace } from '../lib';
import {
  collectAllDependencies,
  isMainModule,
  makeLogger,
  makeThrowValidationError,
  type SetupResult,
  setup,
} from '../lib';
import { findAllExportedPaths } from '../lib/find-all-exported-paths';
import { findAllImportedPackages } from '../lib/find-all-imported-paths';

// bun packages/tooling/custom-scripts/scripts/validate-all.ts
if (isMainModule(import.meta.url)) {
  void setup().then(async (item) => {
    await validate(item);
  });
}

export async function validate(item: SetupResult) {
  const logger = makeLogger('validate');
  const promises: Promise<any>[] = [
    testUtilsCanOnlyBeDevDependency(item.packagesMap),
    testKnownPackagesExistInWorkspace(item.packagesMap),
    shouldOnlyUseDependenciesDefinedInPackageJSON(item.packagesMap),
    shouldOnlyUseDevDependenciesDefinedInPackageJSON(item.packagesMap),
    shouldRespectAllowedWorkspace(item.packagesMap, item.workspaces),
    testBrowserPackagesToNotRelyOnNode(item.packagesMap),
    testJsUtilPackagesToOnlyRelyOnJsUtils(item.packagesMap),
    testSharedConstantsShouldNotHaveDeps(item.packagesMap),
    testUniversalPackagesToRelyOnlyOnUniversal(item.packagesMap),
    testServicePackagesDependencies(item.packagesMap),
    testServiceArchitectureBoundaries(item.packagesMap),
    testWorkspacePackageImportContracts(item.packagesMap),
    validateSameDependencyVersions(item.packagesMap),
  ];

  await Promise.all(promises);
  logger('Validation successful');
}

async function testServiceArchitectureBoundaries(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testServiceArchitectureBoundaries',
  );
  const errors: string[] = [];
  const poorMansDiAllowedPackages = new Set([
    '@learnitive/base-utils',
    '@learnitive/initialize-services',
    '@learnitive/poor-mans-di',
    KNOWN_PACKAGES.testUtils,
  ]);

  for (const [name, pkg] of packageMap.entries()) {
    await pkg.forEachFile(
      async ({ content, filePath }) => {
        const relativeFilePath = path.relative(rootPath, filePath);
        const importPaths = [
          ...findAllImportedPackages(content),
          ...findAllExportedPaths(content),
        ];

        for (const importPath of importPaths) {
          if (
            importPath === '@learnitive/poor-mans-di' &&
            !poorMansDiAllowedPackages.has(name)
          ) {
            errors.push(
              `${relativeFilePath} imports @learnitive/poor-mans-di. Container access belongs in service composition roots or test setup helpers.`,
            );
          }
        }
      },
      (file) => file.isTSFile,
    );
  }

  if (errors.length > 0) {
    throwValidationError(errors.join('\n'));
  }
}

const workspaceImplementationImportPattern =
  /^(@learnitive\/[^/]+)\/src(?:\/|$)/;
const publicRootSubpathEntryNames = new Set([
  'setup.ts',
  'testing.ts',
  'testing-styles.ts',
]);
const starExportPattern = /^\s*export\s+\*\s+from\s+['"][^'"]+['"];?\s*$/;

export function isWorkspaceImplementationImport(
  importPath: string,
  packageNames: ReadonlySet<string>,
): boolean {
  const match = workspaceImplementationImportPattern.exec(importPath);
  const packageName = match?.[1];

  return Boolean(packageName && packageNames.has(packageName));
}

export function isWorkspaceImportContractFile(filePath: string): boolean {
  return /\.(?:cjs|jsx?|mjs|tsx?)$/.test(filePath);
}

export function findStarExportLines(sourceCode: string): number[] {
  const lines: number[] = [];

  for (const [index, line] of sourceCode.split('\n').entries()) {
    if (starExportPattern.test(line)) {
      lines.push(index + 1);
    }
  }

  return lines;
}

function getPublicBarrelEntryPaths(pkg: Package): string[] {
  const entryPaths = new Set<string>();

  for (const entry of [pkg.packageJSON.main, pkg.packageJSON.module]) {
    if (!entry || !isWorkspaceImportContractFile(entry)) {
      continue;
    }

    const filePath = path.resolve(pkg.packagePath, entry);
    if (pkg.getFileHelper(filePath)) {
      entryPaths.add(filePath);
    }
  }

  for (const entryName of publicRootSubpathEntryNames) {
    const filePath = path.resolve(pkg.packagePath, entryName);
    if (pkg.getFileHelper(filePath)) {
      entryPaths.add(filePath);
    }
  }

  return [...entryPaths];
}

async function testWorkspacePackageImportContracts(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testWorkspacePackageImportContracts',
  );
  const packageNames = new Set(packageMap.keys());
  const errors: string[] = [];

  for (const [, pkg] of packageMap.entries()) {
    for (const filePath of getPublicBarrelEntryPaths(pkg)) {
      const relativeFilePath = path.relative(rootPath, filePath);
      const starExportLines = findStarExportLines(
        await fsProm.readFile(filePath, 'utf-8'),
      );

      for (const line of starExportLines) {
        errors.push(
          `${relativeFilePath}:${line} uses export * from a public package barrel. Use explicit named exports so package APIs and unused exports stay visible.`,
        );
      }
    }

    await pkg.forEachFile(
      async ({ content, filePath }) => {
        const relativeFilePath = path.relative(rootPath, filePath);
        const importPaths = [
          ...findAllImportedPackages(content),
          ...findAllExportedPaths(content),
        ];

        for (const importPath of importPaths) {
          if (isWorkspaceImplementationImport(importPath, packageNames)) {
            errors.push(
              `${relativeFilePath} imports private workspace implementation path "${importPath}". Import through the package root or an explicit public subpath instead.`,
            );
          }
        }
      },
      (file) => isWorkspaceImportContractFile(file.filePath),
    );
  }

  if (errors.length > 0) {
    throwValidationError(errors.join('\n'));
  }
}

async function testKnownPackagesExistInWorkspace(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testKnownPackagesExistInWorkspace',
  );

  const missingPackages: string[] = [];

  for (const pkgName of Object.values(KNOWN_PACKAGES)) {
    if (pkgName === KNOWN_PACKAGES.rootPkg) {
      continue;
    }

    if (!packageMap.has(pkgName)) {
      missingPackages.push(pkgName);
    }
  }

  if (missingPackages.length > 0) {
    throwValidationError(
      `The following known packages are missing from the workspace: ${missingPackages.join(
        ', ',
      )}`,
    );
  }
}

async function shouldRespectAllowedWorkspace(
  packageMap: Map<string, Package>,
  workspaceMap: Record<string, Workspace>,
) {
  const throwValidationError = makeThrowValidationError(
    'shouldRespectAllowedWorkspace',
  );

  for (const [name, pkg] of packageMap.entries()) {
    let deps = Object.keys(pkg.workspaceDependencies);
    deps.push(...Object.keys(pkg.workspaceDevDependencies));

    deps = [...new Set(deps)];

    for (const dep of deps) {
      // test utils can be imported anywhere
      if (dep === KNOWN_PACKAGES.testUtils) {
        continue;
      }

      const result = pkg.workspace.allowedWsDependency(
        [...Object.values(workspaceMap)],
        dep,
      );

      if (!result) {
        throwValidationError(
          `Package ${name} imports ${dep}  
          which is not allowed in the workspace. 
          Check "allowedWorkspaces" in the package.json of workspace of ${pkg.workspace.path}`,
        );
      }
    }
  }
}

async function shouldOnlyUseDependenciesDefinedInPackageJSON(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'shouldOnlyUseDependenciesDefinedInPackageJSON',
  );
  for (const [name, pkg] of packageMap.entries()) {
    const deps = [
      ...(await pkg.getImportedPackages((file) => file.isTsSrcFile)),
    ].filter(
      (dep) =>
        !dep.startsWith('node:') &&
        !dep.startsWith('virtual:') &&
        !pkg.learnitivePackageConfig.allowedRuntimeImports?.includes(dep) &&
        dep !== VITEST_PKG_NAME,
    );

    for (const dep of deps) {
      if (!pkg.dependencies[dep]) {
        throwValidationError(
          `Error in Package ${name}
${pkg.packageJSONPath}

imports dependency: ${dep} which is not defined in package.json's "dependencies"`,
        );
      }
    }
  }
}

async function shouldOnlyUseDevDependenciesDefinedInPackageJSON(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'shouldOnlyUseDevDependenciesDefinedInPackageJSON',
  );

  for (const [name, pkg] of packageMap.entries()) {
    const deps = [
      ...(await pkg.getImportedPackages((file) => file.isTestFile)),
    ].filter((dep) => !dep.startsWith('node:') && dep !== VITEST_PKG_NAME);

    for (const dep of deps) {
      if (!pkg.devDependencies?.[dep] && !pkg.dependencies[dep]) {
        throwValidationError(
          `Package ${name} ${pkg.packageJSONPath}  imports ${dep} which is not in the workspace`,
        );
      }
    }
  }
}

async function testBrowserPackagesToNotRelyOnNode(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testBrowserPackagesToNotRelyOnNode',
  );

  for (const [name, pkg] of packageMap.entries()) {
    if (pkg.env !== 'browser') {
      continue;
    }

    const deps = Object.values(pkg.workspaceDependencies);

    for (const dep of deps) {
      if (dep.env === 'nodejs') {
        throwValidationError(
          `Browser Package ${name} ${pkg.packageJSONPath}
imports ${dep.name} which is a node package. Either move it to devDep or make it a browser package`,
        );
      }
    }
  }
}

async function testJsUtilPackagesToOnlyRelyOnJsUtils(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testJsUtilPackagesToOnlyRelyOnJsUtils',
  );

  for (const [name, pkg] of packageMap.entries()) {
    if (pkg.learnitivePackageConfig.kind !== 'js-util') {
      continue;
    }

    const deps = [
      ...Object.values(pkg.workspaceDependencies),
      ...Object.values(pkg.workspaceDevDependencies),
    ];

    for (const dep of deps) {
      if (dep.learnitivePackageConfig.kind !== 'js-util') {
        throwValidationError(
          `Package  ${name} ${pkg.packageJSONPath} kind: ${pkg.learnitivePackageConfig.kind} imports "${dep.name}" which is a ${dep.learnitivePackageConfig.kind} package. 
          Js-util package can only import other js-util packages`,
        );
      }
    }
  }
}

async function testUniversalPackagesToRelyOnlyOnUniversal(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testUniversalPackagesToRelyOnlyOnUniversal',
  );

  for (const [name, pkg] of packageMap.entries()) {
    if (pkg.env !== 'universal') {
      continue;
    }

    const deps = Object.values(pkg.workspaceDependencies);

    for (const dep of deps) {
      if (dep.env !== 'universal') {
        throwValidationError(
          `Universal Package ${name} ${pkg.packageJSONPath}
imports ${dep.name} which is a ${dep.env} package. Universal package can only import other universal packages`,
        );
      }
    }
  }
}

async function testServicePackagesDependencies(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testServicePackagesDependencies',
  );

  for (const [name, pkg] of packageMap.entries()) {
    if (!pkg.learnitivePackageConfig.kind.startsWith('service-')) {
      continue;
    }

    const pkgKind = pkg.learnitivePackageConfig.kind;
    const pkgKindIndex = serviceKindOrders.indexOf(pkgKind);

    if (pkgKindIndex === -1) {
      throwValidationError(
        `Package ${name} has unknown service kind ${pkgKind}`,
      );
      continue;
    }

    const allowedKinds = serviceKindOrders.slice(0, pkgKindIndex + 1);

    const deps = [
      ...Object.values(pkg.workspaceDependencies),
      ...Object.values(pkg.workspaceDevDependencies),
    ];

    for (const dep of deps) {
      if (dep.learnitivePackageConfig.kind.startsWith('service-')) {
        const depKind = dep.learnitivePackageConfig.kind;

        if (!allowedKinds.includes(depKind)) {
          throwValidationError(
            `Package ${name} of kind ${pkgKind} cannot depend on ${dep.name} of kind ${depKind}`,
          );
        }
      }
    }
  }
}

/**
 * Ensure constants package does not have any dependencies,
 * having dependencies in constants package will end up causing extra code
 * to be bundled in the inlined scripts.
 * Keeping constants lean helps us use it at multiple places.
 */
async function testSharedConstantsShouldNotHaveDeps(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testSharedConstantsShouldNotHaveDeps',
  );

  const allowedDependencies = [
    KNOWN_PACKAGES.sharedTypedPkg,
    KNOWN_PACKAGES.colorSchemeManagerPkg,
  ];

  let visited = false;

  for (const [name, pkg] of packageMap.entries()) {
    if (pkg.name !== KNOWN_PACKAGES.constantsPkg) {
      continue;
    }

    visited = true;
    const deps = Object.keys(pkg.dependencies);

    const invalidDeps = deps.filter(
      (dep) => !allowedDependencies.includes(dep),
    );

    const hasDevDependencies =
      Object.keys(pkg.devDependencies ?? {}).length > 0;

    if (invalidDeps.length > 0 || hasDevDependencies) {
      throwValidationError(
        `Package "${name}" ${pkg.packageJSONPath} can only depend on the following packages: ${allowedDependencies.join(', ')}.`,
      );
    }
  }

  if (!visited) {
    throwValidationError(
      `Package "${KNOWN_PACKAGES.constantsPkg}" is not present in the workspace`,
    );
  }
}

async function validateSameDependencyVersions(
  packagesMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'validateSameDependencyVersions',
  );

  const dependencyMap = await collectAllDependencies(packagesMap);
  const errors: string[] = [];

  for (const [depName, { versions, usedBy }] of Object.entries(dependencyMap)) {
    if (versions.length > 1) {
      errors.push(
        `Dependency "${depName}" is used with multiple versions: ${versions.join(
          ', ',
        )}.\nUsed by packages: ${usedBy.join(', ')}`,
      );
    }
  }

  if (errors.length > 0) {
    throwValidationError(
      `Multiple versions of the same dependency are used:\n\n${errors.join(
        '\n\n',
      )}`,
    );
  }
}

async function testUtilsCanOnlyBeDevDependency(
  packageMap: Map<string, Package>,
) {
  const throwValidationError = makeThrowValidationError(
    'testUtilsCanOnlyBeDevDependency',
  );

  for (const [name, pkg] of packageMap.entries()) {
    // Skip test-utils package itself
    if (pkg.name === KNOWN_PACKAGES.testUtils) {
      continue;
    }

    // Check if test-utils is in dependencies
    if (pkg.workspaceDependencies[KNOWN_PACKAGES.testUtils]) {
      throwValidationError(
        `Package ${name} has test-utils as a dependency. test-utils can only be used as a devDependency.`,
      );
    }
  }
}
