// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only

/**
 * Pack-side workspace.yaml reader.
 *
 * Locates the project root (via kernel primitive) and returns the
 * `software_delivery` section of `workspace.yaml`. Loose typing on purpose —
 * the authoritative schema lives in `@lumenflow/core`; this helper exists to
 * keep the pack core-free for Option 1b prep work (INIT-058).
 */

import { readFileSync } from 'node:fs';
import { join } from 'node:path';

import { findProjectRoot } from '@lumenflow/kernel/primitives/project-root';
import { WORKSPACE_CONFIG_FILE_NAME } from '@lumenflow/kernel/primitives/workspace-constants';
import { parse as parseYaml } from 'yaml';

import {
  AGENTS_DEFAULTS,
  CLEANUP_DEFAULTS,
  CLOUD_DEFAULTS,
  DIRECTORIES_DEFAULTS,
  ESCALATION_DEFAULTS,
  EXPERIMENTAL_DEFAULTS,
  GATES_DEFAULTS,
  GIT_DEFAULTS,
  INITIATIVE_ID_DEFAULTS,
  LUMENFLOW_CONFIG_DEFAULTS,
  MEMORY_DEFAULTS,
  STATE_PATHS_DEFAULTS,
  TELEMETRY_DEFAULTS,
  UI_DEFAULTS,
  WU_DEFAULTS,
  WU_ID_DEFAULTS,
  YAML_DEFAULTS,
  type AgentsConfig,
  type CleanupConfig,
  type CloudConfig,
  type DirectoriesConfig,
  type EscalationConfig,
  type EventArchivalConfig,
  type ExperimentalConfig,
  type GatesConfig,
  type GitConfig,
  type IdFormatConfig,
  type LumenflowConfig,
  type MemoryConfig,
  type PackageManager,
  type PushRetryConfig,
  type StatePathsConfig,
  type TelemetryConfig,
  type TestRunner,
  type UiConfig,
  type WuConfig,
  type YamlConfig,
} from './schemas/lumenflow-config-schema-types.js';

export type SoftwareDeliveryConfig = Record<string, unknown>;

/**
 * Read `workspace.yaml` relative to the given `cwd`'s project root and return
 * the `software_delivery` section (or an empty object if absent/empty).
 *
 * @throws If the project root cannot be located or the YAML is malformed.
 */
export function readSoftwareDeliveryConfig(cwd: string): SoftwareDeliveryConfig {
  const root = findProjectRoot(cwd);
  const raw = readFileSync(join(root, WORKSPACE_CONFIG_FILE_NAME), 'utf8');
  const doc = parseYaml(raw) as Record<string, unknown> | null | undefined;
  const section = doc?.['software_delivery'];
  if (section && typeof section === 'object' && !Array.isArray(section)) {
    return section as SoftwareDeliveryConfig;
  }
  return {};
}

/**
 * Re-export pack-local directory/state-path type aliases. These mirror
 * `@lumenflow/core/schemas/directories-config`; drift is enforced by the
 * integration-tests parity tests.
 */
export type WorkspaceDirectories = DirectoriesConfig;
export type WorkspaceStatePaths = StatePathsConfig;
export type WorkspaceEventArchivalConfig = EventArchivalConfig;

function readObjectRecord(cwd: string, key: string): Record<string, unknown> {
  const section = readSoftwareDeliveryConfig(cwd);
  const value = section[key];
  if (value && typeof value === 'object' && !Array.isArray(value)) {
    return value as Record<string, unknown>;
  }
  return {};
}

/**
 * Return the typed directory map for the workspace rooted at `cwd`.
 *
 * User-provided values in `software_delivery.directories` are merged over the
 * built-in defaults. Unknown keys in the user's YAML are ignored by the return
 * type but pass through the spread — the authoritative schema validation still
 * lives in core's `getConfig()` pathway.
 */
export function getDirectories(cwd: string): WorkspaceDirectories {
  const overrides = readObjectRecord(cwd, 'directories') as Partial<WorkspaceDirectories>;
  return { ...DIRECTORIES_DEFAULTS, ...overrides };
}

/**
 * Return the typed state-paths map for the workspace rooted at `cwd`.
 *
 * User-provided values in `software_delivery.state` are merged over the
 * built-in defaults. `eventArchival` is merged as a nested object so a partial
 * override (e.g. only `archiveAfter`) preserves the remaining defaults.
 */
export function getStatePaths(cwd: string): WorkspaceStatePaths {
  const overrides = readObjectRecord(cwd, 'state') as Partial<WorkspaceStatePaths>;
  const { eventArchival: overrideArchival, ...flatOverrides } = overrides;
  const eventArchivalOverrides =
    overrideArchival && typeof overrideArchival === 'object' && !Array.isArray(overrideArchival)
      ? (overrideArchival as Partial<WorkspaceEventArchivalConfig>)
      : {};
  return {
    ...STATE_PATHS_DEFAULTS,
    ...flatOverrides,
    eventArchival: {
      ...STATE_PATHS_DEFAULTS.eventArchival,
      ...eventArchivalOverrides,
    },
  };
}

function mergeGit(overrides: Partial<GitConfig> | undefined): GitConfig {
  if (!overrides) return { ...GIT_DEFAULTS, push_retry: { ...GIT_DEFAULTS.push_retry } };
  const { push_retry: overridePushRetry, ...flatOverrides } = overrides;
  const pushRetryOverrides =
    overridePushRetry && typeof overridePushRetry === 'object' && !Array.isArray(overridePushRetry)
      ? (overridePushRetry as Partial<PushRetryConfig>)
      : {};
  return {
    ...GIT_DEFAULTS,
    ...flatOverrides,
    push_retry: {
      ...GIT_DEFAULTS.push_retry,
      ...pushRetryOverrides,
    },
  };
}

function mergeWu(overrides: Partial<WuConfig> | undefined): WuConfig {
  if (!overrides) return { ...WU_DEFAULTS, brief: { ...WU_DEFAULTS.brief } };
  const { brief: overrideBrief, ...flatOverrides } = overrides;
  const briefOverrides =
    overrideBrief && typeof overrideBrief === 'object' && !Array.isArray(overrideBrief)
      ? (overrideBrief as Partial<WuConfig['brief']>)
      : {};
  return {
    ...WU_DEFAULTS,
    ...flatOverrides,
    brief: {
      ...WU_DEFAULTS.brief,
      ...briefOverrides,
    },
  };
}

function mergeSimple<T>(defaults: T, overrides: unknown): T {
  if (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) {
    return { ...defaults, ...(overrides as object) } as T;
  }
  return { ...defaults };
}

function mergeTelemetry(overrides: Partial<TelemetryConfig> | undefined): TelemetryConfig {
  if (!overrides) {
    return { methodology: { ...TELEMETRY_DEFAULTS.methodology } };
  }
  const methodology =
    overrides.methodology && typeof overrides.methodology === 'object'
      ? { ...TELEMETRY_DEFAULTS.methodology, ...overrides.methodology }
      : { ...TELEMETRY_DEFAULTS.methodology };
  return { methodology };
}

function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
  return typeof value === 'string' && (allowed as readonly string[]).includes(value)
    ? (value as T)
    : fallback;
}

/**
 * Return the fully-typed pack-local `LumenflowConfig` for the workspace rooted
 * at `cwd`.
 *
 * Mirrors the shape produced by `@lumenflow/core`'s
 * `getConfig({ projectRoot: cwd }).parse({})` for every field exposed on
 * `LumenflowConfig`. Drift from core is enforced by
 * `integration-tests/tests/config-parity.test.ts`.
 *
 * Behaviour parity with core:
 * - Reads `workspace.yaml` relative to `cwd`'s project root via the kernel
 *   `findProjectRoot` primitive.
 * - Returns defaults when `workspace.yaml` is absent or lacks a
 *   `software_delivery` block (graceful fallback, matches core's
 *   non-`strictWorkspace` mode).
 * - User-provided values are spread over defaults at the section level, with
 *   nested merges for sections that have nested objects (state.eventArchival,
 *   git.push_retry, wu.brief, telemetry.methodology).
 */
export function getConfig(cwd: string): LumenflowConfig {
  let section: SoftwareDeliveryConfig;
  try {
    section = readSoftwareDeliveryConfig(cwd);
  } catch {
    // Graceful fallback — matches core's non-strictWorkspace behaviour when
    // workspace.yaml is absent or unreadable.
    return cloneDefaults();
  }

  return {
    version: typeof section['version'] === 'string' ? section['version'] : '1.0.0',
    directories: getDirectories(cwd),
    state: getStatePaths(cwd),
    git: mergeGit(section['git'] as Partial<GitConfig> | undefined),
    wu: mergeWu(section['wu'] as Partial<WuConfig> | undefined),
    wuId: mergeSimple<IdFormatConfig>(WU_ID_DEFAULTS, section['wuId']),
    initiativeId: mergeSimple<IdFormatConfig>(INITIATIVE_ID_DEFAULTS, section['initiativeId']),
    gates: mergeSimple<GatesConfig>(GATES_DEFAULTS, section['gates']),
    memory: mergeSimple<MemoryConfig>(MEMORY_DEFAULTS, section['memory']),
    ui: mergeSimple<UiConfig>(UI_DEFAULTS, section['ui']),
    yaml: mergeSimple<YamlConfig>(YAML_DEFAULTS, section['yaml']),
    agents: mergeSimple<AgentsConfig>(AGENTS_DEFAULTS, section['agents']),
    experimental: mergeSimple<ExperimentalConfig>(EXPERIMENTAL_DEFAULTS, section['experimental']),
    cleanup: mergeSimple<CleanupConfig>(CLEANUP_DEFAULTS, section['cleanup']),
    telemetry: mergeTelemetry(section['telemetry'] as Partial<TelemetryConfig> | undefined),
    methodology:
      section['methodology'] && typeof section['methodology'] === 'object'
        ? (section['methodology'] as Record<string, unknown>)
        : undefined,
    cloud: mergeSimple<CloudConfig>(CLOUD_DEFAULTS, section['cloud']),
    lanes:
      section['lanes'] && typeof section['lanes'] === 'object'
        ? (section['lanes'] as Record<string, unknown>)
        : undefined,
    escalation: mergeSimple<EscalationConfig>(ESCALATION_DEFAULTS, section['escalation']),
    package_manager: pickEnum<PackageManager>(
      section['package_manager'],
      ['pnpm', 'npm', 'yarn', 'bun'] as const,
      'pnpm',
    ),
    test_runner: pickEnum<TestRunner>(
      section['test_runner'],
      ['vitest', 'jest', 'mocha'] as const,
      'vitest',
    ),
    build_command:
      typeof section['build_command'] === 'string' ? section['build_command'] : 'pnpm build',
  };
}

function cloneDefaults(): LumenflowConfig {
  return {
    ...LUMENFLOW_CONFIG_DEFAULTS,
    directories: { ...DIRECTORIES_DEFAULTS },
    state: {
      ...STATE_PATHS_DEFAULTS,
      eventArchival: { ...STATE_PATHS_DEFAULTS.eventArchival },
    },
    git: { ...GIT_DEFAULTS, push_retry: { ...GIT_DEFAULTS.push_retry } },
    wu: { ...WU_DEFAULTS, brief: { ...WU_DEFAULTS.brief } },
    wuId: { ...WU_ID_DEFAULTS },
    initiativeId: { ...INITIATIVE_ID_DEFAULTS },
    gates: { ...GATES_DEFAULTS },
    memory: { ...MEMORY_DEFAULTS },
    ui: { ...UI_DEFAULTS },
    yaml: { ...YAML_DEFAULTS },
    agents: { ...AGENTS_DEFAULTS },
    experimental: { ...EXPERIMENTAL_DEFAULTS },
    cleanup: { ...CLEANUP_DEFAULTS },
    telemetry: { methodology: { ...TELEMETRY_DEFAULTS.methodology } },
    cloud: { ...CLOUD_DEFAULTS, env_signals: [...CLOUD_DEFAULTS.env_signals] },
    escalation: { ...ESCALATION_DEFAULTS },
  };
}

// Re-export pack-local schema types for downstream consumers migrating off
// `@lumenflow/core/lumenflow-config-schema`.
export type {
  AgentsConfig,
  CleanupConfig,
  CloudConfig,
  DirectoriesConfig,
  EscalationConfig,
  EventArchivalConfig,
  ExperimentalConfig,
  GatesConfig,
  GitConfig,
  IdFormatConfig,
  LumenflowConfig,
  MemoryConfig,
  PackageManager,
  PushRetryConfig,
  StatePathsConfig,
  TelemetryConfig,
  TestRunner,
  UiConfig,
  WuConfig,
  YamlConfig,
} from './schemas/lumenflow-config-schema-types.js';
