/**
 * Server-side devops info collector.
 *
 * Usage in a Node backend:
 * ```ts
 * import { collectDevopsInfo, readServiceVersion, dependencyCheck } from '@bytelyst/devops/server';
 *
 * app.get('/api/devops/info', requireAdmin, async (_req, res) => {
 *   const info = await collectDevopsInfo({
 *     productId: 'invttrdg',
 *     serviceName: 'trading-backend',
 *     serviceVersion: readServiceVersion(import.meta.url),
 *     dependencyChecks: [
 *       () => dependencyCheck('cosmos', () => cosmosPing()),
 *     ],
 *   });
 *   res.json(info);
 * });
 *
 * app.get('/api/devops/version', (_req, res) => res.json(getBuildInfo()));
 * ```
 */

import os from 'node:os';
import process from 'node:process';
import { readFileSync, existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { BuildInfo, ConfigInfo, DependencyStatus, DevopsInfo, RuntimeInfo } from './types.js';

export type { BuildInfo, ConfigInfo, DependencyStatus, DevopsInfo, RuntimeInfo };

/** Options for {@link collectDevopsInfo}. */
export interface CollectOptions {
  productId: string;
  serviceName: string;
  serviceVersion: string;
  safeEnvPrefixes?: string[];
  dependencyChecks?: Array<() => Promise<DependencyStatus>>;
  extra?: Record<string, unknown>;
}

const DEFAULT_SAFE_PREFIXES = [
  'NODE_ENV',
  'NODE_VERSION',
  'PRODUCT_ID',
  'PLATFORM_',
  'VITE_',
  'BYTELYST_',
  'COSMOS_DATABASE',
  'PORT',
];

const SECRET_PATTERN = /SECRET|KEY|TOKEN|PASSWORD|PRIVATE/i;

/**
 * Collect runtime + build info. Safe to call from any request handler.
 *
 * Never returns secret values; only env var *keys* are listed.
 */
export async function collectDevopsInfo(opts: CollectOptions): Promise<DevopsInfo> {
  const build = getBuildInfo();
  const runtime = getRuntimeInfo();
  const config = getConfigInfo(opts);
  const dependencies = opts.dependencyChecks
    ? await runDependencyChecks(opts.dependencyChecks)
    : undefined;
  return { build, runtime, config, dependencies, extra: opts.extra };
}

/**
 * Build-only info — safe to expose without auth (commit SHA + version).
 */
export function getBuildInfo(): BuildInfo {
  const full = process.env.BYTELYST_COMMIT_SHA_FULL || process.env.BYTELYST_COMMIT_SHA || null;
  const short = process.env.BYTELYST_COMMIT_SHA || (full ? full.substring(0, 7) : null);
  return {
    commitSha: short,
    commitShaFull: full,
    branch: process.env.BYTELYST_BRANCH || null,
    builtAt: process.env.BYTELYST_BUILT_AT || null,
    commitAuthor: process.env.BYTELYST_COMMIT_AUTHOR || null,
    commitMessage: process.env.BYTELYST_COMMIT_MESSAGE || null,
    dockerImage: process.env.BYTELYST_DOCKER_IMAGE || null,
  };
}

/** Runtime info (process + container). */
export function getRuntimeInfo(): RuntimeInfo {
  const uptimeSeconds = Math.floor(process.uptime());
  const mem = process.memoryUsage();
  return {
    uptimeSeconds,
    uptimeHuman: humanizeUptime(uptimeSeconds),
    nodeVersion: process.version,
    platform: process.platform,
    arch: process.arch,
    pid: process.pid,
    hostname: os.hostname(),
    memoryMb: Math.round(mem.rss / 1024 / 1024),
    heapMb: Math.round(mem.heapUsed / 1024 / 1024),
    startedAt: new Date(Date.now() - uptimeSeconds * 1000).toISOString(),
  };
}

/** Configuration-level info (safe, non-secret env keys only). */
export function getConfigInfo(opts: CollectOptions): ConfigInfo {
  const prefixes = opts.safeEnvPrefixes ?? DEFAULT_SAFE_PREFIXES;
  const envKeys = Object.keys(process.env)
    .filter(k => prefixes.some(p => (p.endsWith('_') ? k.startsWith(p) : k === p)))
    .filter(k => !SECRET_PATTERN.test(k))
    .sort();
  return {
    productId: opts.productId,
    serviceName: opts.serviceName,
    serviceVersion: opts.serviceVersion,
    nodeEnv: process.env.NODE_ENV || 'development',
    envKeys,
  };
}

/**
 * Walk up from the given path/import.meta.url to find package.json and return its `version`.
 * Falls back to `npm_package_version` env var, then to "0.0.0".
 */
export function readServiceVersion(fromPathOrUrl?: string): string {
  let dir: string;
  if (!fromPathOrUrl) {
    dir = process.cwd();
  } else if (fromPathOrUrl.startsWith('file://')) {
    dir = dirname(fileURLToPath(fromPathOrUrl));
  } else {
    dir = dirname(fromPathOrUrl);
  }
  for (let i = 0; i < 8; i++) {
    const pkg = join(dir, 'package.json');
    if (existsSync(pkg)) {
      try {
        const data = JSON.parse(readFileSync(pkg, 'utf8')) as { version?: string };
        if (data.version) return data.version;
      } catch {
        /* fall through */
      }
    }
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return process.env.npm_package_version || '0.0.0';
}

/**
 * Wrap an async health check with timing + timeout + error-to-status conversion.
 */
export async function dependencyCheck(
  name: string,
  check: () => Promise<unknown>,
  timeoutMs = 5000
): Promise<DependencyStatus> {
  const start = Date.now();
  try {
    await Promise.race([
      check(),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs)
      ),
    ]);
    return { name, ok: true, latencyMs: Date.now() - start };
  } catch (e) {
    return {
      name,
      ok: false,
      latencyMs: Date.now() - start,
      detail: e instanceof Error ? e.message : String(e),
    };
  }
}

/** Convenience HTTP ping — returns ok if status < 500. */
export async function httpDependencyCheck(
  name: string,
  url: string,
  timeoutMs = 5000
): Promise<DependencyStatus> {
  return dependencyCheck(
    name,
    async () => {
      const res = await fetch(url);
      if (res.status >= 500) {
        throw new Error(`HTTP ${res.status}`);
      }
    },
    timeoutMs
  );
}

async function runDependencyChecks(
  checks: Array<() => Promise<DependencyStatus>>
): Promise<DependencyStatus[]> {
  const results = await Promise.allSettled(checks.map(c => c()));
  return results.map((r, i) =>
    r.status === 'fulfilled'
      ? r.value
      : { name: `check-${i}`, ok: false, detail: String((r as PromiseRejectedResult).reason) }
  );
}

function humanizeUptime(seconds: number): string {
  if (seconds < 60) return `${seconds}s`;
  const mins = Math.floor(seconds / 60);
  if (mins < 60) return `${mins}m ${seconds % 60}s`;
  const hrs = Math.floor(mins / 60);
  if (hrs < 24) return `${hrs}h ${mins % 60}m`;
  const days = Math.floor(hrs / 24);
  return `${days}d ${hrs % 24}h ${mins % 60}m`;
}
