import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { resolvePackageRegistry } from './config-store.js';

type JsonRecord = Record<string, unknown>;

export interface IRegistryCredential {
  readonly token: string;
  readonly username: string;
  readonly email?: string;
  readonly expiresAt?: string;
}

interface ICredentialFile {
  readonly registries: Record<string, IRegistryCredential>;
}

function asRecord(value: unknown): JsonRecord | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }
  return value as JsonRecord;
}

function matrixHome(cwd: string): string {
  return path.resolve(
    cwd,
    process.env.MATRIX_HOME
      ?? path.join(cwd, '.matrix'),
  );
}

function resolveCredentialPath(cwd: string, credentialsPath?: string): string {
  return path.resolve(
    cwd,
    credentialsPath ?? path.join(matrixHome(cwd), 'credentials', 'registry.json'),
  );
}

function readCredentialFile(filePath: string): ICredentialFile {
  if (!fs.existsSync(filePath)) {
    return { registries: {} };
  }
  if (fs.statSync(filePath).isDirectory()) {
    throw new Error(`MX_AUTH_CREDENTIALS_PATH_IS_DIRECTORY: ${filePath}`);
  }

  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
  const record = asRecord(parsed);
  const registries = asRecord(record?.registries) ?? {};
  const normalized: Record<string, IRegistryCredential> = {};

  for (const [registry, value] of Object.entries(registries)) {
    const entry = asRecord(value);
    if (!entry) continue;
    if (typeof entry.token !== 'string' || typeof entry.username !== 'string') continue;
    normalized[registry] = {
      token: entry.token,
      username: entry.username,
      email: typeof entry.email === 'string' ? entry.email : undefined,
      expiresAt: typeof entry.expiresAt === 'string' ? entry.expiresAt : undefined,
    };
  }

  return { registries: normalized };
}

function writeCredentialFile(filePath: string, payload: ICredentialFile): void {
  if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
    throw new Error(`MX_AUTH_CREDENTIALS_PATH_IS_DIRECTORY: ${filePath}`);
  }
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
}

function readNpmTokenFromEnvironment(): string | null {
  const token = process.env.NODE_AUTH_TOKEN ?? process.env.NPM_TOKEN;
  if (typeof token !== 'string') {
    return null;
  }
  const trimmed = token.trim();
  return trimmed.length > 0 ? trimmed : null;
}

function configuredNpmUserConfigPath(): string | undefined {
  const value = process.env.NPM_CONFIG_USERCONFIG ?? process.env.npm_config_userconfig;
  if (typeof value !== 'string') {
    return undefined;
  }
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function npmrcSearchPaths(cwd: string): readonly string[] {
  const configuredUserConfig = configuredNpmUserConfigPath();
  if (configuredUserConfig) {
    return [configuredUserConfig];
  }
  return [
    path.join(cwd, '.npmrc'),
    path.join(os.homedir(), '.npmrc'),
  ];
}

function readNpmTokenFromNpmrc(filePath: string, registry: string): string | null {
  if (!fs.existsSync(filePath)) {
    return null;
  }
  const lines = fs.readFileSync(filePath, 'utf8')
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter((line) => line.length > 0 && !line.startsWith('#') && !line.startsWith(';'));
  const registryKeys = npmRegistryAuthKeys(registry);
  for (const line of lines) {
    for (const key of registryKeys) {
      const prefix = `${key}:_authToken=`;
      if (line.startsWith(prefix)) {
        const token = line.slice(prefix.length).trim();
        return token.length > 0 ? token : null;
      }
    }
  }
  return null;
}

function npmRegistryAuthKeys(registry: string): readonly string[] {
  const parsed = new URL(registry);
  const host = parsed.host;
  const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : `${parsed.pathname}/`;
  const segments = pathname.split('/').filter((segment) => segment.length > 0);
  const keys = new Set<string>();
  for (let length = segments.length; length >= 0; length -= 1) {
    const partial = length === 0 ? '/' : `/${segments.slice(0, length).join('/')}/`;
    keys.add(`//${host}${partial}`);
  }
  return [...keys];
}

function readNpmCredential(cwd: string, registry: string): IRegistryCredential | null {
  const envToken = readNpmTokenFromEnvironment();
  if (envToken) {
    return { username: 'npm-token', token: envToken };
  }
  for (const filePath of npmrcSearchPaths(cwd)) {
    const token = readNpmTokenFromNpmrc(filePath, registry);
    if (token) {
      return { username: 'npm-token', token };
    }
  }
  return null;
}

export function saveRegistryCredential(
  cwd: string,
  credential: IRegistryCredential,
  options: {
    readonly registry?: string;
    readonly credentialsPath?: string;
  } = {},
): { readonly registry: string; readonly credentialsPath: string } {
  const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
  const filePath = resolveCredentialPath(cwd, options.credentialsPath);
  const payload = readCredentialFile(filePath);
  payload.registries[registry] = credential;
  writeCredentialFile(filePath, payload);
  return { registry, credentialsPath: filePath };
}

export function removeRegistryCredential(
  cwd: string,
  options: {
    readonly registry?: string;
    readonly credentialsPath?: string;
  } = {},
): { readonly registry: string; readonly removed: boolean; readonly credentialsPath: string } {
  const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
  const filePath = resolveCredentialPath(cwd, options.credentialsPath);
  const payload = readCredentialFile(filePath);
  const removed = Boolean(payload.registries[registry]);
  if (removed) {
    delete payload.registries[registry];
    writeCredentialFile(filePath, payload);
  }
  return { registry, removed, credentialsPath: filePath };
}

export function readRegistryCredential(
  cwd: string,
  options: {
    readonly registry?: string;
    readonly credentialsPath?: string;
  } = {},
): {
  readonly registry: string;
  readonly credentialsPath: string;
  readonly credential: IRegistryCredential | null;
} {
  const registry = resolvePackageRegistry(options.registry, { cwd }).registry;
  const filePath = resolveCredentialPath(cwd, options.credentialsPath);
  const payload = readCredentialFile(filePath);
  return {
    registry,
    credentialsPath: filePath,
    credential: payload.registries[registry] ?? readNpmCredential(cwd, registry),
  };
}
