/**
 * GitFlow Config — Read/write .claude/gitflow/config.json v2.1.0.
 * Self-contained: imports only from local lib + node built-ins.
 */

import { readFile, writeFile, mkdir, copyFile } from 'fs/promises';
import { existsSync } from 'fs';
import { join, dirname, resolve } from 'path';
import type { GitFlowConfig, WorktreeMode } from './types.js';
import { normalizeForPlatform, normalizeForStorage } from './paths.js';
import { detectPlatform } from './platform.js';
import { execGit, getGitCommonDir } from './git.js';

/** Legacy location (≤5.4 stored the config INSIDE the develop worktree). */
const CONFIG_RELATIVE = '.claude/gitflow/config.json';

/**
 * The canonical config path = `<repo-root>/.gitflow/config.json`, where the repo
 * root is the PARENT of the git common dir. The common dir is shared by every
 * worktree, so this resolves to the SAME file from main, develop or any
 * feature/* worktree — the config is repo-global LOCAL state, not branch state,
 * and living outside every working tree it can never be committed by accident.
 * Pure: only path math (the git call lives in resolveConfigPath).
 */
export function repoRootConfigFromCommonDir(commonDir: string, cwd: string): string {
  const root = dirname(resolve(cwd, commonDir));
  return join(root, '.gitflow', 'config.json');
}

/**
 * Parse `git worktree list --porcelain` and return the path of the worktree
 * checked out on `develop` (where ≤5.4 stored the config), or null. Pure.
 */
export function developWorktreePath(worktreeListPorcelain: string): string | null {
  let currentPath = '';
  for (const line of worktreeListPorcelain.split('\n')) {
    if (line.startsWith('worktree ')) currentPath = line.slice(9);
    if (line.includes('branch refs/heads/develop') && currentPath) return currentPath;
  }
  return null;
}

export type ConfigResolution =
  | { action: 'root'; path: string }
  | { action: 'migrate'; from: string; to: string }
  | { action: 'legacy'; path: string }
  | { action: 'none' };

/**
 * Decide where to read the config from, given whether the canonical root config
 * already exists and whether a legacy (in-worktree) config was found. When only
 * a legacy config exists, it is migrated to the root. Pure (no I/O).
 */
export function decideConfigResolution(opts: {
  rootPath: string | null;
  rootExists: boolean;
  legacyPath: string | null;
}): ConfigResolution {
  const { rootPath, rootExists, legacyPath } = opts;
  if (rootPath && rootExists) return { action: 'root', path: rootPath };
  if (!legacyPath) return { action: 'none' };
  if (rootPath) return { action: 'migrate', from: legacyPath, to: rootPath };
  return { action: 'legacy', path: legacyPath };
}

export async function resolveConfigPath(startDir?: string): Promise<string | null> {
  const cwd = startDir || process.cwd();

  const commonDir = await getGitCommonDir(cwd);
  const rootPath = commonDir ? repoRootConfigFromCommonDir(commonDir, cwd) : null;
  const rootExists = !!(rootPath && existsSync(rootPath));

  // Only hunt legacy locations when the canonical one isn't there yet.
  const legacyPath = rootExists ? null : await findLegacyConfigPath(cwd);
  const decision = decideConfigResolution({ rootPath, rootExists, legacyPath });

  switch (decision.action) {
    case 'root':
    case 'legacy':
      return decision.path;
    case 'none':
      return null;
    case 'migrate':
      // One-time, best-effort, idempotent migration legacy → root. On any
      // failure (perms, racing run) fall back to the legacy path so reads never break.
      try {
        await mkdir(dirname(decision.to), { recursive: true });
        await copyFile(decision.from, decision.to);
        return decision.to;
      } catch {
        return decision.from;
      }
  }
}

/** Locate a legacy (≤5.4) in-worktree config: cwd, the develop worktree, then the toplevel. */
async function findLegacyConfigPath(cwd: string): Promise<string | null> {
  const local = join(cwd, CONFIG_RELATIVE);
  if (existsSync(local)) return local;

  const wtResult = await execGit(['worktree', 'list', '--porcelain'], cwd);
  if (wtResult.exitCode === 0) {
    const devPath = developWorktreePath(wtResult.stdout);
    if (devPath) {
      const devConfig = join(devPath, CONFIG_RELATIVE);
      if (existsSync(devConfig)) return devConfig;
    }
  }

  const rootResult = await execGit(['rev-parse', '--show-toplevel'], cwd);
  if (rootResult.exitCode === 0) {
    const topConfig = join(rootResult.stdout, CONFIG_RELATIVE);
    if (existsSync(topConfig)) return topConfig;
  }

  return null;
}

export async function readConfig(configPath?: string): Promise<GitFlowConfig> {
  const path = configPath || await resolveConfigPath();
  if (!path) {
    throw new Error('GitFlow config not found. Run gitflow init first.');
  }

  const content = await readFile(path, 'utf-8');
  const raw = JSON.parse(content);
  return applyDefaults(raw);
}

export async function writeConfig(config: GitFlowConfig, configPath: string): Promise<void> {
  const stored = structuredClone(config);

  stored.repository.rootFolder = normalizeForStorage(stored.repository.rootFolder);
  if (stored.worktrees.structure) {
    const s = stored.worktrees.structure;
    s.main = normalizeForStorage(s.main);
    s.develop = normalizeForStorage(s.develop);
    s.features = normalizeForStorage(s.features);
    s.releases = normalizeForStorage(s.releases);
    s.hotfixes = normalizeForStorage(s.hotfixes);
  }
  if (stored.workspace?.path) {
    stored.workspace.path = normalizeForStorage(stored.workspace.path);
  }

  await mkdir(dirname(configPath), { recursive: true });
  await writeFile(configPath, JSON.stringify(stored, null, 2) + '\n');
}

/**
 * Patch ONLY `versioning.current` in the on-disk config, preserving everything
 * else verbatim (the 4.x `sed` equivalent — no applyDefaults, no path
 * normalization). Returns the config path, or null when no config is found.
 * Used by finish to record the next development version.
 */
export async function updateCurrentVersion(newVersion: string, startDir?: string): Promise<string | null> {
  const path = await resolveConfigPath(startDir);
  if (!path) return null;
  const raw = JSON.parse(await readFile(path, 'utf-8')) as Record<string, unknown>;
  const versioning = (raw.versioning && typeof raw.versioning === 'object' ? raw.versioning : {}) as Record<string, unknown>;
  versioning.current = newVersion;
  raw.versioning = versioning;
  await writeFile(path, JSON.stringify(raw, null, 2) + '\n');
  return path;
}

export async function readConfigForPlatform(configPath?: string): Promise<GitFlowConfig> {
  const config = await readConfig(configPath);
  const { platform } = detectPlatform();

  config.repository.rootFolder = normalizeForPlatform(config.repository.rootFolder, platform);
  if (config.worktrees.structure) {
    const s = config.worktrees.structure;
    s.main = normalizeForPlatform(s.main, platform);
    s.develop = normalizeForPlatform(s.develop, platform);
    s.features = normalizeForPlatform(s.features, platform);
    s.releases = normalizeForPlatform(s.releases, platform);
    s.hotfixes = normalizeForPlatform(s.hotfixes, platform);
  }
  if (config.workspace?.path) {
    config.workspace.path = normalizeForPlatform(config.workspace.path, platform);
  }

  return config;
}

export function createDefaultConfig(overrides?: Partial<GitFlowConfig>): GitFlowConfig {
  const { platform, shell } = detectPlatform();
  const base: GitFlowConfig = {
    version: '2.1.0',
    platform: { detected: platform, shell, detectedAt: new Date().toISOString() },
    workspace: { path: '', name: '' },
    repository: {
      name: '',
      rootFolder: '',
      nameVariants: { pascalCaseDot: '', pascalCase: '', kebabCase: '', snakeCase: '', displayName: '' },
      defaultBranch: 'main',
      remoteUrl: '',
    },
    git: {
      provider: 'unknown',
      branches: { main: 'main', develop: 'develop' },
      prefixes: { feature: 'feature/', release: 'release/', hotfix: 'hotfix/' },
    },
    worktrees: {
      enabled: true,
      mode: 'organized' as WorktreeMode,
      structure: { main: '', develop: '', features: '', releases: '', hotfixes: '' },
    },
    versioning: { strategy: 'semver', current: '0.0.0', tagPrefix: 'v', sources: ['csproj', 'package.json', 'VERSION'] },
    efcore: {
      enabled: true,
      validateOnCommit: true,
      blockDestructive: true,
      migrationNaming: '{context}_v{version}_{sequence}_{Description}',
      migrationNamingSquash: '{context}_v{version}',
      squashBeforePR: true,
    },
    workflow: { push: { afterCommit: 'worktree' }, pr: { autoLabels: true, requireReview: true } },
    language: { code: 'en' },
  };

  if (overrides) {
    return deepMerge(base as unknown as Record<string, unknown>, overrides as unknown as Record<string, unknown>) as unknown as GitFlowConfig;
  }
  return base;
}

function applyDefaults(raw: Record<string, unknown>): GitFlowConfig {
  const defaults = createDefaultConfig();
  return deepMerge(defaults as unknown as Record<string, unknown>, raw) as unknown as GitFlowConfig;
}

function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> {
  const result = { ...target };
  for (const key of Object.keys(source)) {
    const sVal = source[key];
    const tVal = target[key];
    if (sVal && typeof sVal === 'object' && !Array.isArray(sVal) && tVal && typeof tVal === 'object' && !Array.isArray(tVal)) {
      result[key] = deepMerge(tVal as Record<string, unknown>, sVal as Record<string, unknown>);
    } else if (sVal !== undefined) {
      result[key] = sVal;
    }
  }
  return result;
}
