import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { env } from '../config/env.js';
import { resolveMainProjectRoot } from './main-project-root-service.js';

export type WorkServerBuildInfo = {
  version: string;
  buildId: string;
  builtAt: string;
};

export type WorkServerSourceChangeInfo = {
  changedAt: string;
  path: string;
};

const MODULE_DIR_PATH = path.dirname(fileURLToPath(import.meta.url));
const WORK_SERVER_ROOT_PATH = path.resolve(MODULE_DIR_PATH, '..', '..');
const SOURCE_TARGET_PATH_NAMES = ['src', 'scripts', 'package.json', 'tsconfig.json'] as const;
const WORK_SERVER_SOURCE_EXCLUDED_FILE_PATTERNS = [/\.test\.[^/]+$/i, /\.spec\.[^/]+$/i] as const;

function normalizeRootPath(value: string | null | undefined) {
  const normalized = String(value ?? '').trim();
  return normalized ? path.resolve(normalized) : null;
}

function resolveSourceTargetRoots() {
  const mainProjectRoot = normalizeRootPath(resolveMainProjectRoot());

  if (mainProjectRoot) {
    const mirroredWorkServerRoot = path.join(mainProjectRoot, 'etc', 'servers', 'work-server');

    if (fs.existsSync(mirroredWorkServerRoot)) {
      return [mirroredWorkServerRoot];
    }
  }

  return [WORK_SERVER_ROOT_PATH];
}

function resolveBuildInfoDirectoryPath(rootPath: string, configuredDistDir: string) {
  return path.resolve(rootPath, configuredDistDir);
}

export function resolveWorkServerBuildInfoFilePaths(options?: {
  workServerRootPath?: string;
  mainProjectRoot?: string | null;
  configuredDistDir?: string | null;
}) {
  const workServerRootPath = path.resolve(options?.workServerRootPath ?? WORK_SERVER_ROOT_PATH);
  const configuredDistDir = String(options?.configuredDistDir ?? env.WORK_SERVER_DIST_DIR ?? 'dist').trim() || 'dist';
  const mainProjectRoot = normalizeRootPath(
    options?.mainProjectRoot ?? resolveMainProjectRoot(),
  );
  const candidates = [
    path.join(resolveBuildInfoDirectoryPath(workServerRootPath, configuredDistDir), 'build-info.json'),
    path.join(workServerRootPath, 'dist', 'build-info.json'),
  ];

  if (mainProjectRoot) {
    const mirroredWorkServerRoot = path.join(mainProjectRoot, 'etc', 'servers', 'work-server');
    candidates.push(path.join(resolveBuildInfoDirectoryPath(mirroredWorkServerRoot, configuredDistDir), 'build-info.json'));
    candidates.push(path.join(mirroredWorkServerRoot, 'dist', 'build-info.json'));
  }

  return candidates.filter((candidate, index, array) => array.indexOf(candidate) === index);
}

function normalizeBuildInfo(value: unknown): WorkServerBuildInfo | null {
  if (!value || typeof value !== 'object') {
    return null;
  }

  const candidate = value as Partial<Record<keyof WorkServerBuildInfo, unknown>>;

  if (
    typeof candidate.version !== 'string' ||
    typeof candidate.buildId !== 'string' ||
    typeof candidate.builtAt !== 'string'
  ) {
    return null;
  }

  const builtAt = new Date(candidate.builtAt);

  if (Number.isNaN(builtAt.getTime())) {
    return null;
  }

  return {
    version: candidate.version,
    buildId: candidate.buildId,
    builtAt: builtAt.toISOString(),
  };
}

function readBuildInfoFromDiskSync(filePath: string) {
  try {
    if (!fs.existsSync(filePath)) {
      return null;
    }

    return normalizeBuildInfo(JSON.parse(fs.readFileSync(filePath, 'utf8')));
  } catch {
    return null;
  }
}

export async function readLatestWorkServerBuildInfo() {
  let latestBuildInfo: WorkServerBuildInfo | null = null;

  for (const candidatePath of resolveWorkServerBuildInfoFilePaths()) {
    try {
      const buildInfo = normalizeBuildInfo(JSON.parse(await readFile(candidatePath, 'utf8')));

      if (!buildInfo) {
        continue;
      }

      if (!latestBuildInfo || buildInfo.builtAt > latestBuildInfo.builtAt) {
        latestBuildInfo = buildInfo;
      }
    } catch {
      continue;
    }
  }

  return latestBuildInfo;
}

const runtimeWorkServerBuildInfo = readBuildInfoFromDiskSync(
  path.join(resolveBuildInfoDirectoryPath(WORK_SERVER_ROOT_PATH, env.WORK_SERVER_DIST_DIR), 'build-info.json'),
);

export function getRuntimeWorkServerBuildInfo() {
  return runtimeWorkServerBuildInfo;
}

function isExcludedWorkServerSourcePath(rootPath: string, targetPath: string) {
  const relativePath = path.relative(rootPath, targetPath).replace(/\\/g, '/');
  const baseName = path.basename(relativePath);
  return WORK_SERVER_SOURCE_EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(baseName));
}

async function findLatestSourceChangeInPath(rootPath: string, targetPath: string): Promise<WorkServerSourceChangeInfo | null> {
  try {
    if (isExcludedWorkServerSourcePath(rootPath, targetPath)) {
      return null;
    }

    const stats = await fs.promises.stat(targetPath);

    if (stats.isFile()) {
      return {
        changedAt: stats.mtime.toISOString(),
        path: path.relative(rootPath, targetPath) || path.basename(targetPath),
      };
    }

    if (!stats.isDirectory()) {
      return null;
    }

    const entries = await fs.promises.readdir(targetPath, { withFileTypes: true });
    let latest: WorkServerSourceChangeInfo | null = null;

    for (const entry of entries) {
      if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.docker') {
        continue;
      }

      const childPath = path.join(targetPath, entry.name);
      const childLatest = await findLatestSourceChangeInPath(rootPath, childPath);

      if (!childLatest) {
        continue;
      }

      if (!latest || childLatest.changedAt > latest.changedAt) {
        latest = childLatest;
      }
    }

    return latest;
  } catch {
    return null;
  }
}

export async function readLatestWorkServerSourceChange() {
  let latest: WorkServerSourceChangeInfo | null = null;

  for (const rootPath of resolveSourceTargetRoots()) {
    for (const targetName of SOURCE_TARGET_PATH_NAMES) {
      const candidate = await findLatestSourceChangeInPath(rootPath, path.join(rootPath, targetName));

      if (!candidate) {
        continue;
      }

      if (!latest || candidate.changedAt > latest.changedAt) {
        latest = candidate;
      }
    }
  }

  return latest;
}
