import { execFile } from 'node:child_process';
import { access, copyFile } from 'node:fs/promises';
import { constants as fsConstants } from 'node:fs';
import { promisify } from 'node:util';
import { getEnv } from '../config/env.js';

const execFileAsync = promisify(execFile);
const gitCredentialSourcePath = '/root/.git-credentials';
const gitCredentialCachePath = '/tmp/work-server-git-credentials';

async function prepareWritableCredentialStore() {
  try {
    await access(gitCredentialSourcePath, fsConstants.R_OK);
    await copyFile(gitCredentialSourcePath, gitCredentialCachePath);
    return gitCredentialCachePath;
  } catch {
    return null;
  }
}

export type GitAutomationConfig = {
  repoPath: string;
  releaseBranch: string;
  mainBranch: string;
};

async function runGit(repoPath: string, args: string[]) {
  const credentialStorePath = await prepareWritableCredentialStore();
  const gitArgs = ['-c', `safe.directory=${repoPath}`];
  const env = getEnv();

  if (credentialStorePath) {
    gitArgs.push('-c', `credential.helper=store --file=${credentialStorePath}`);
  }

  await execFileAsync('git', ['-c', `safe.directory=${repoPath}`, '-C', repoPath, 'config', 'user.name', env.PLAN_GIT_USER_NAME], {
    encoding: 'utf8',
  });
  await execFileAsync('git', ['-c', `safe.directory=${repoPath}`, '-C', repoPath, 'config', 'user.email', env.PLAN_GIT_USER_EMAIL], {
    encoding: 'utf8',
  });

  const { stdout, stderr } = await execFileAsync('git', [...gitArgs, '-C', repoPath, ...args], {
    encoding: 'utf8',
  });

  return {
    stdout: stdout.trim(),
    stderr: stderr.trim(),
  };
}

async function assertBranchExists(repoPath: string, branchName: string) {
  try {
    await runGit(repoPath, ['rev-parse', '--verify', branchName]);
  } catch {
    throw new Error(
      `${branchName} 브랜치를 찾을 수 없습니다. 먼저 ${branchName} 브랜치를 생성한 뒤 다시 시도해 주세요.`,
    );
  }
}

async function hasLocalBranch(repoPath: string, branchName: string) {
  try {
    await runGit(repoPath, ['rev-parse', '--verify', '--quiet', branchName]);
    return true;
  } catch {
    return false;
  }
}

async function hasRemoteBranch(repoPath: string, branchName: string) {
  try {
    await runGit(repoPath, ['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${branchName}`]);
    return true;
  } catch {
    return false;
  }
}

async function syncBranchWithRemote(repoPath: string, branchName: string) {
  await runGit(repoPath, ['fetch', 'origin', branchName]);

  if (!(await hasRemoteBranch(repoPath, branchName))) {
    return;
  }

  if (!(await hasLocalBranch(repoPath, branchName))) {
    await runGit(repoPath, ['branch', branchName, `origin/${branchName}`]);
  }

  try {
    await runGit(repoPath, ['switch', branchName]);
  } catch {
    await runGit(repoPath, ['switch', '-C', branchName, `origin/${branchName}`]);
  }

  await runGit(repoPath, ['reset', '--hard', `origin/${branchName}`]);
}

export async function assertCleanWorktree(repoPath: string) {
  const { stdout } = await runGit(repoPath, ['status', '--porcelain']);

  if (stdout) {
    throw new Error('Git 작업 디렉터리가 깨끗하지 않습니다. 변경 사항을 정리한 뒤 다시 시도해 주세요.');
  }
}

export async function cleanAutomationWorktree(repoPath: string) {
  await runGit(repoPath, ['reset', '--hard']);
  await runGit(repoPath, ['clean', '-fd']);
}

export async function ensureBranchExists(config: GitAutomationConfig, branchName: string, releaseTarget?: string) {
  const baseBranch = config.mainBranch;

  await assertCleanWorktree(config.repoPath);
  await syncBranchWithRemote(config.repoPath, baseBranch);
  await assertBranchExists(config.repoPath, baseBranch);
  await runGit(config.repoPath, ['switch', baseBranch]);
  await runGit(config.repoPath, ['switch', '-C', branchName]);
}

export async function pushBranch(repoPath: string, branchName: string, setUpstream = false) {
  await runGit(repoPath, setUpstream ? ['push', '-u', 'origin', branchName] : ['push', 'origin', branchName]);
}

async function deleteLocalBranch(repoPath: string, branchName: string) {
  try {
    await runGit(repoPath, ['branch', '-D', branchName]);
  } catch {
    return;
  }
}

async function deleteRemoteBranch(repoPath: string, branchName: string) {
  try {
    await runGit(repoPath, ['push', 'origin', '--delete', branchName]);
  } catch {
    return;
  }
}

export async function commitAllChanges(repoPath: string, message: string) {
  await runGit(repoPath, ['add', '-A']);
  await runGit(repoPath, ['commit', '-m', message]);
}

export async function hasWorkingTreeChanges(repoPath: string) {
  const { stdout } = await runGit(repoPath, ['status', '--porcelain']);
  return Boolean(stdout);
}

async function ensureLocalBranchFromRemote(repoPath: string, branchName: string) {
  if (await hasLocalBranch(repoPath, branchName)) {
    await runGit(repoPath, ['switch', branchName]);
    return;
  }

  if (await hasRemoteBranch(repoPath, branchName)) {
    await runGit(repoPath, ['switch', '-C', branchName, `origin/${branchName}`]);
    return;
  }

  await runGit(repoPath, ['switch', '-C', branchName]);
}

export async function syncMainProjectBranchForReservedRestart(
  repoPath: string,
  branchName: string,
  commitMessage: string,
) {
  const env = getEnv();
  const useLocalMainMode = Boolean(env.PLAN_LOCAL_MAIN_MODE);

  if (useLocalMainMode) {
    return {
      branchName,
      commitMessage: null,
      committed: false,
      head: null,
      syncMode: 'local' as const,
    };
  }

  await runGit(repoPath, ['fetch', 'origin', branchName]);
  await ensureLocalBranchFromRemote(repoPath, branchName);

  const hadChanges = await hasWorkingTreeChanges(repoPath);
  if (hadChanges) {
    await commitAllChanges(repoPath, commitMessage);
  }

  await runGit(repoPath, ['pull', '--rebase', 'origin', branchName]);
  await pushBranch(repoPath, branchName);

  const { stdout: head } = await runGit(repoPath, ['rev-parse', 'HEAD']);

  return {
    branchName,
    commitMessage: hadChanges ? commitMessage : null,
    committed: hadChanges,
    head,
    syncMode: 'remote' as const,
  };
}

function isHotfixBranch(branchName: string) {
  return /^hotfix\//.test(branchName);
}

function isReleaseBranch(branchName: string, config: GitAutomationConfig) {
  return branchName === config.releaseBranch || /^release([/-]|$)/.test(branchName);
}

function shouldSquashMerge(sourceBranch: string, targetBranch: string, config: GitAutomationConfig) {
  return isHotfixBranch(sourceBranch) && (
    targetBranch === config.mainBranch || isReleaseBranch(targetBranch, config)
  );
}

function assertAllowedMergeDirection(
  config: GitAutomationConfig,
  sourceBranch: string,
  targetBranch: string,
) {
  if (
    targetBranch === config.mainBranch &&
    sourceBranch !== config.releaseBranch &&
    sourceBranch !== config.mainBranch &&
    !isHotfixBranch(sourceBranch)
  ) {
    throw new Error(
      `브랜치 전략 위반: ${sourceBranch} -> ${targetBranch} 직접 머지는 허용되지 않습니다. release 반영 후 main에 반영해 주세요.`,
    );
  }
}

export async function mergeBranch(
  config: GitAutomationConfig,
  sourceBranch: string,
  targetBranch: string,
) {
  assertAllowedMergeDirection(config, sourceBranch, targetBranch);
  await assertCleanWorktree(config.repoPath);
  await syncBranchWithRemote(config.repoPath, targetBranch);
  await assertBranchExists(config.repoPath, targetBranch);
  await assertBranchExists(config.repoPath, sourceBranch);

  if (sourceBranch === config.releaseBranch || sourceBranch === config.mainBranch) {
    await syncBranchWithRemote(config.repoPath, sourceBranch);
  }

  await runGit(config.repoPath, ['switch', targetBranch]);
  if (shouldSquashMerge(sourceBranch, targetBranch, config)) {
    await runGit(config.repoPath, ['merge', '--squash', sourceBranch]);
    await runGit(config.repoPath, ['commit', '-m', `merge: ${sourceBranch} -> ${targetBranch} (squash)`]);
    return;
  }

  await runGit(config.repoPath, ['merge', '--no-ff', sourceBranch, '-m', `merge: ${sourceBranch} -> ${targetBranch}`]);
}

export async function mergeBranchToRelease(
  config: GitAutomationConfig,
  branchName: string,
  releaseTarget?: string,
) {
  const baseBranch = releaseTarget || config.releaseBranch;
  await mergeBranch(config, branchName, baseBranch);
}

export async function mergeReleaseToMain(
  config: GitAutomationConfig,
  releaseTarget?: string,
) {
  const baseBranch = releaseTarget || config.releaseBranch;
  await mergeBranch(config, baseBranch, config.mainBranch);
}

export async function mergeIssueBranchToMain(
  config: GitAutomationConfig,
  branchName: string,
) {
  throw new Error(
    `브랜치 전략 위반: ${branchName} -> ${config.mainBranch} 직접 머지는 비활성화되었습니다. release 브랜치를 통해 반영해 주세요.`,
  );
}

export async function pullMainProjectBranch(repoPath: string, branchName: string) {
  await assertCleanWorktree(repoPath);
  await runGit(repoPath, ['fetch', 'origin', branchName]);

  try {
    await runGit(repoPath, ['switch', branchName]);
  } catch {
    await runGit(repoPath, ['switch', '-C', branchName, `origin/${branchName}`]);
  }

  await runGit(repoPath, ['pull', '--ff-only', 'origin', branchName]);
}

export async function recreateReleaseBranchFromMain(
  config: GitAutomationConfig,
  releaseTarget?: string,
) {
  const targetBranch = releaseTarget || config.releaseBranch;

  if (targetBranch === config.mainBranch) {
    throw new Error('release 브랜치를 main 브랜치와 동일하게 재생성할 수 없습니다.');
  }

  await assertCleanWorktree(config.repoPath);
  await syncBranchWithRemote(config.repoPath, config.mainBranch);
  await assertBranchExists(config.repoPath, config.mainBranch);
  await runGit(config.repoPath, ['switch', config.mainBranch]);
  await deleteLocalBranch(config.repoPath, targetBranch);
  await deleteRemoteBranch(config.repoPath, targetBranch);
  await runGit(config.repoPath, ['switch', '-C', targetBranch, config.mainBranch]);
  await pushBranch(config.repoPath, targetBranch, true);
}
