import os from 'os';
import path from 'path';

import type { MoaConfig, MoaModelConfig } from '../moa.js';
import type { AgentType } from '../types.js';
import { getEnv, listConfiguredEnvKeys } from '../env.js';

import type { AppConfig, RoleModelConfig } from './schema.js';

const CANONICAL_DISCORD_CHANNEL_TOKEN_KEYS = new Set([
  'DISCORD_OWNER_BOT_TOKEN',
  'DISCORD_REVIEWER_BOT_TOKEN',
  'DISCORD_ARBITER_BOT_TOKEN',
]);

const CANONICAL_SESSION_COMMAND_KEYS = new Set([
  'SESSION_COMMAND_ALLOWED_SENDERS',
]);

function readText(key: string): string | undefined {
  return getEnv(key);
}

function readNonEmptyText(key: string): string | undefined {
  const value = getEnv(key);
  return value === '' ? undefined : value;
}

function readBoolean(key: string, fallback = false): boolean {
  const value = readText(key);
  if (value == null || value === '') return fallback;
  return value === 'true';
}

function readBooleanUnlessFalse(key: string, fallback = true): boolean {
  const value = readText(key);
  if (value == null || value === '') return fallback;
  return value !== 'false';
}

function readInteger(key: string, fallback: number): number {
  const value = readNonEmptyText(key);
  if (value == null) return fallback;
  const parsed = parseInt(value, 10);
  return Number.isFinite(parsed) ? parsed : fallback;
}

function readIntegerAtLeast(
  key: string,
  fallback: number,
  minimum: number,
): number {
  return Math.max(minimum, readInteger(key, fallback) || fallback);
}

function readPercent(key: string, fallback: number): number {
  const value = readInteger(key, fallback);
  return Math.min(100, Math.max(0, value));
}

function readAgentType(
  key: string,
  fallback?: AgentType,
): AgentType | undefined {
  const value = readText(key);
  if (value === 'codex' || value === 'claude-code') return value;
  return fallback;
}

function buildRoleModelConfig(envPrefix: string): RoleModelConfig {
  return {
    model: readNonEmptyText(`${envPrefix}_MODEL`),
    effort: readNonEmptyText(`${envPrefix}_EFFORT`),
    fallbackEnabled: readText(`${envPrefix}_FALLBACK_ENABLED`) !== 'false',
  };
}

function parseMoaReferenceModels(): MoaModelConfig[] {
  const names = (readText('MOA_REF_MODELS') ?? '')
    .split(',')
    .map((value) => value.trim())
    .filter(Boolean);

  return names
    .map((name) => {
      const prefix = `MOA_${name.toUpperCase()}`;
      const model = readNonEmptyText(`${prefix}_MODEL`) ?? '';
      const baseUrl = readNonEmptyText(`${prefix}_BASE_URL`) ?? '';
      const apiKey = readNonEmptyText(`${prefix}_API_KEY`) ?? '';
      if (!model || !baseUrl || !apiKey) return null;
      const rawFormat = readText(`${prefix}_API_FORMAT`) ?? '';
      const apiFormat: 'openai' | 'anthropic' =
        rawFormat === 'anthropic' ? 'anthropic' : 'openai';
      return { name, model, baseUrl, apiKey, apiFormat };
    })
    .filter((value): value is MoaModelConfig => value !== null);
}

function buildMoaConfig(): MoaConfig {
  const referenceModels = parseMoaReferenceModels();
  return {
    enabled: readText('MOA_ENABLED') === 'true' && referenceModels.length > 0,
    referenceModels,
  };
}

function escapeRegex(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function normalizeServiceId(
  serviceId: string,
  codexMainServiceId: string,
): string {
  return serviceId === 'codex' ? codexMainServiceId : serviceId;
}

function assertNoLegacyEnvAliasesConfigured(): void {
  const configuredAliases = listConfiguredEnvKeys().filter((key) => {
    if (key.startsWith('DISCORD_') && key.endsWith('_BOT_TOKEN')) {
      return !CANONICAL_DISCORD_CHANNEL_TOKEN_KEYS.has(key);
    }
    if (key.startsWith('SESSION_COMMAND_')) {
      return !CANONICAL_SESSION_COMMAND_KEYS.has(key);
    }
    return false;
  });

  if (configuredAliases.length === 0) {
    return;
  }

  throw new Error(
    `Legacy env aliases are no longer supported; remove or rename (${configuredAliases.join(', ')}) to the canonical keys DISCORD_OWNER_BOT_TOKEN, DISCORD_REVIEWER_BOT_TOKEN, DISCORD_ARBITER_BOT_TOKEN, SESSION_COMMAND_ALLOWED_SENDERS`,
  );
}

function buildPathsConfig(
  projectRoot: string,
  homeDir: string,
): AppConfig['paths'] {
  return {
    projectRoot,
    homeDir,
    senderAllowlistPath: path.join(
      homeDir,
      '.config',
      'ejclaw',
      'sender-allowlist.json',
    ),
    storeDir: path.resolve(
      readNonEmptyText('EJCLAW_STORE_DIR') ?? path.join(projectRoot, 'store'),
    ),
    groupsDir: path.resolve(
      readNonEmptyText('EJCLAW_GROUPS_DIR') ?? path.join(projectRoot, 'groups'),
    ),
    dataDir: path.resolve(
      readNonEmptyText('EJCLAW_DATA_DIR') ?? path.join(projectRoot, 'data'),
    ),
    cacheDir: path.resolve(
      readNonEmptyText('EJCLAW_CACHE_DIR') ?? path.join(projectRoot, 'cache'),
    ),
  };
}

export function loadConfig(): AppConfig {
  assertNoLegacyEnvAliasesConfigured();

  const assistantName = readText('ASSISTANT_NAME') ?? 'Andy';
  const assistantSlug = assistantName.trim().toLowerCase();

  const claudeServiceId = readText('CLAUDE_SERVICE_ID') ?? 'claude';
  const codexMainServiceId = readText('CODEX_MAIN_SERVICE_ID') ?? 'codex-main';
  const codexReviewServiceId =
    readText('CODEX_REVIEW_SERVICE_ID') ?? 'codex-review';
  const serviceId = readText('SERVICE_ID') ?? assistantSlug;
  const serviceSessionScope = normalizeServiceId(serviceId, codexMainServiceId);

  const ownerAgentType = readAgentType('OWNER_AGENT_TYPE', 'codex') ?? 'codex';
  const reviewerAgentType =
    readAgentType('REVIEWER_AGENT_TYPE', 'claude-code') ?? 'claude-code';
  const arbiterAgentType = readAgentType('ARBITER_AGENT_TYPE');

  const projectRoot = process.cwd();
  const homeDir = readNonEmptyText('HOME') ?? os.homedir();

  const rawMaxRoundTrips = readText('PAIRED_MAX_ROUND_TRIPS');
  const maxRoundTrips =
    rawMaxRoundTrips === '0'
      ? Infinity
      : readInteger('PAIRED_MAX_ROUND_TRIPS', 1000);

  return {
    assistant: {
      name: assistantName,
      hasOwnNumber: readBoolean('ASSISTANT_HAS_OWN_NUMBER', false),
      slug: assistantSlug,
      triggerPattern: new RegExp(`^@${escapeRegex(assistantName)}\\b`, 'i'),
    },
    service: {
      id: serviceId,
      sessionScope: serviceSessionScope,
      claudeId: claudeServiceId,
      codexMainId: codexMainServiceId,
      codexReviewId: codexReviewServiceId,
    },
    paths: buildPathsConfig(projectRoot, homeDir),
    runtime: {
      pollInterval: 2000,
      schedulerPollInterval: 60000,
      failoverMinDurationMs: 3 * 60 * 60 * 1000,
      agentTimeout: readInteger('AGENT_TIMEOUT', 1_800_000),
      agentMaxOutputSize: readInteger('AGENT_MAX_OUTPUT_SIZE', 10_485_760),
      ipcPollInterval: 1000,
      idleTimeout: readInteger('IDLE_TIMEOUT', 1_800_000),
      maxConcurrentAgents: readIntegerAtLeast('MAX_CONCURRENT_AGENTS', 5, 1),
      recoveryConcurrentAgents: readInteger('RECOVERY_CONCURRENT_AGENTS', 3),
      recoveryStaggerMs: readInteger('RECOVERY_STAGGER_MS', 2000),
      recoveryDurationMs: readInteger('RECOVERY_DURATION_MS', 60000),
    },
    logging: {
      level: readText('LOG_LEVEL') ?? 'info',
      isTestEnv:
        readText('VITEST') === 'true' || readText('NODE_ENV') === 'test',
    },
    paired: {
      ownerAgentType,
      reviewerAgentType,
      reviewerServiceIdForType:
        reviewerAgentType === 'claude-code'
          ? claudeServiceId
          : codexReviewServiceId,
      arbiterAgentType,
      arbiterServiceId: arbiterAgentType
        ? (readText('ARBITER_SERVICE_ID') ?? codexReviewServiceId)
        : null,
      carryForwardLatestOwnerFinal: readBoolean(
        'PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL',
        false,
      ),
      forceFreshClaudeReviewerSessionInUnsafeHostMode: readBoolean(
        'PAIRED_FORCE_FRESH_CLAUDE_REVIEWER_SESSION',
        false,
      ),
      agentLanguage: readText('AGENT_LANGUAGE') ?? '',
      arbiterDeadlockThreshold: readInteger('ARBITER_DEADLOCK_THRESHOLD', 2),
      arbiterMaxInterventions: readInteger('ARBITER_MAX_INTERVENTIONS', 1),
      maxRoundTrips,
    },
    models: {
      owner: buildRoleModelConfig('OWNER'),
      reviewer: buildRoleModelConfig('REVIEWER'),
      arbiter: buildRoleModelConfig('ARBITER'),
    },
    providers: {
      claudeDefaultModel: readText('CLAUDE_MODEL') ?? 'claude',
      codexDefaultModel: readText('CODEX_MODEL') ?? 'codex',
    },
    moa: buildMoaConfig(),
    status: {
      channelId: readText('STATUS_CHANNEL_ID') ?? '',
      // Base periodic refresh. Event-driven updates (new message, agent
      // activity change) refresh immediately between these ticks.
      updateInterval: 60000,
      usageUpdateInterval: 60000,
      showRooms: readBooleanUnlessFalse('STATUS_SHOW_ROOMS', true),
      showRoomDetails: readBooleanUnlessFalse('STATUS_SHOW_ROOM_DETAILS', true),
      usageDashboardEnabled: readText('USAGE_DASHBOARD') === 'true',
      timezone:
        readText('TZ') ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
    },
    webDashboard: {
      enabled: readBoolean('WEB_DASHBOARD_ENABLED', false),
      host: readNonEmptyText('WEB_DASHBOARD_HOST') ?? '127.0.0.1',
      port: readIntegerAtLeast('WEB_DASHBOARD_PORT', 8734, 1),
      staticDir: path.resolve(
        readNonEmptyText('WEB_DASHBOARD_STATIC_DIR') ??
          path.join(projectRoot, 'apps', 'dashboard', 'dist'),
      ),
      token: readNonEmptyText('WEB_DASHBOARD_TOKEN') ?? '',
    },
    codexWarmup: {
      enabled: readBoolean('CODEX_WARMUP_ENABLED', false),
      prompt:
        readNonEmptyText('CODEX_WARMUP_PROMPT') ??
        'Reply exactly OK. Do not run tools.',
      model:
        readNonEmptyText('CODEX_WARMUP_MODEL') ??
        readNonEmptyText('CODEX_MODEL') ??
        'codex',
      intervalMs: readIntegerAtLeast('CODEX_WARMUP_INTERVAL_MS', 300000, 60000),
      minIntervalMs: readIntegerAtLeast(
        'CODEX_WARMUP_MIN_INTERVAL_MS',
        18300000,
        60000,
      ),
      staggerMs: Math.max(0, readInteger('CODEX_WARMUP_STAGGER_MS', 1800000)),
      maxUsagePct: readPercent('CODEX_WARMUP_MAX_USAGE_PCT', 0),
      maxD7UsagePct: readPercent('CODEX_WARMUP_MAX_D7_USAGE_PCT', 0),
      commandTimeoutMs: readIntegerAtLeast(
        'CODEX_WARMUP_COMMAND_TIMEOUT_MS',
        120000,
        10000,
      ),
      failureCooldownMs: readIntegerAtLeast(
        'CODEX_WARMUP_FAILURE_COOLDOWN_MS',
        21600000,
        60000,
      ),
      maxConsecutiveFailures: readIntegerAtLeast(
        'CODEX_WARMUP_MAX_CONSECUTIVE_FAILURES',
        2,
        1,
      ),
    },
    sessionCommands: {
      allowedSenders: new Set(
        (readText('SESSION_COMMAND_ALLOWED_SENDERS') ?? '')
          .split(',')
          .map((value) => value.trim())
          .filter(Boolean),
      ),
    },
    deltaHandoff: {
      enabled: readText('DELTA_HANDOFF_ENABLED') === 'true',
      canaryGroups: new Set(
        (readText('DELTA_HANDOFF_CANARY_GROUPS') ?? '')
          .split(',')
          .map((value) => value.trim())
          .filter(Boolean),
      ),
    },
  };
}
