import { spawn } from 'node:child_process';
import { mkdir, open, readFile, rm, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { resolveMainProjectRoot } from './main-project-root-service.js';

const TEST_SERVER_DEPLOYMENT_LOCK_STALE_MS = 20 * 60 * 1000;
const TEST_SERVER_DEPLOYMENT_LOG_LIMIT = 4000;
const TEST_SERVER_DEPLOYMENT_CLEANUP_DELAY_MS = 15_000;

export type TestServerDeploymentStepKey = 'commit-main-worktree' | 'push-origin-main' | 'build-test-app' | 'deploy-test-server';
export type TestServerDeploymentStepStatus = 'pending' | 'running' | 'completed' | 'failed';

export type TestServerDeploymentStepSnapshot = {
  key: TestServerDeploymentStepKey;
  status: TestServerDeploymentStepStatus;
  detail: string | null;
  updatedAt: string | null;
};

export type TestServerDeploymentPhase =
  | 'idle'
  | 'commit-main-worktree'
  | 'push-origin-main'
  | 'build-test-app'
  | 'deploy-test-server'
  | 'completed'
  | 'failed';

export type TestServerDeploymentSnapshot = {
  status: 'idle' | 'running' | 'completed' | 'failed';
  phase: TestServerDeploymentPhase;
  summary: string | null;
  startedAt: string | null;
  updatedAt: string | null;
  completedAt: string | null;
  lastError: string | null;
  logExcerpt: string | null;
  steps: TestServerDeploymentStepSnapshot[];
};

type TestServerDeploymentStateFilePayload = {
  status?: unknown;
  phase?: unknown;
  summary?: unknown;
  startedAt?: unknown;
  updatedAt?: unknown;
  completedAt?: unknown;
  lastError?: unknown;
  logExcerpt?: unknown;
  steps?: unknown;
};

type RestartLockPayload = {
  startedAt: string;
  key: 'test';
  pid: number;
};

const TEST_SERVER_DEPLOYMENT_STEP_KEYS: TestServerDeploymentStepKey[] = [
  'commit-main-worktree',
  'push-origin-main',
  'build-test-app',
  'deploy-test-server',
];

function normalizeDateTimeValue(value: string | null | undefined) {
  const normalized = value?.trim();

  if (!normalized) {
    return null;
  }

  const parsed = new Date(normalized);
  return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
}

function trimPreview(value: string | null | undefined, maxLength = 220) {
  const normalized = value?.replace(/\s+/g, ' ').trim() ?? '';

  if (!normalized) {
    return null;
  }

  return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 3)}...` : normalized;
}

function getTestServerDeploymentStatePath() {
  return path.join(resolveMainProjectRoot(), 'etc', 'servers', 'work-server', '.docker', 'runtime', 'test-deployment-state.json');
}

function getTestServerDeploymentLockPath() {
  return path.join(resolveMainProjectRoot(), 'etc', 'servers', 'work-server', '.docker', 'runtime', 'test-deployment-in-progress.json');
}

function buildEmptyTestServerDeploymentSnapshot(): TestServerDeploymentSnapshot {
  return {
    status: 'idle',
    phase: 'idle',
    summary: null,
    startedAt: null,
    updatedAt: null,
    completedAt: null,
    lastError: null,
    logExcerpt: null,
    steps: TEST_SERVER_DEPLOYMENT_STEP_KEYS.map((key) => ({
      key,
      status: 'pending',
      detail: null,
      updatedAt: null,
    })),
  };
}

function normalizeTestServerDeploymentStepKey(value: unknown): TestServerDeploymentStepKey | null {
  return TEST_SERVER_DEPLOYMENT_STEP_KEYS.includes(value as TestServerDeploymentStepKey)
    ? (value as TestServerDeploymentStepKey)
    : null;
}

function normalizeTestServerDeploymentPhase(value: unknown): TestServerDeploymentPhase {
  return value === 'commit-main-worktree'
    || value === 'push-origin-main'
    || value === 'build-test-app'
    || value === 'deploy-test-server'
    || value === 'completed'
    || value === 'failed'
    ? value
    : 'idle';
}

function normalizeTestServerDeploymentStatus(value: unknown): TestServerDeploymentSnapshot['status'] {
  return value === 'running' || value === 'completed' || value === 'failed' ? value : 'idle';
}

function normalizeTestServerDeploymentSteps(value: unknown) {
  const fallback = buildEmptyTestServerDeploymentSnapshot().steps;

  if (!Array.isArray(value)) {
    return fallback;
  }

  const normalizedByKey = new Map<TestServerDeploymentStepKey, TestServerDeploymentStepSnapshot>();

  value.forEach((item) => {
    if (!item || typeof item !== 'object') {
      return;
    }

    const candidate = item as Record<string, unknown>;
    const key = normalizeTestServerDeploymentStepKey(candidate.key);

    if (!key) {
      return;
    }

    normalizedByKey.set(key, {
      key,
      status:
        candidate.status === 'running'
        || candidate.status === 'completed'
        || candidate.status === 'failed'
        || candidate.status === 'pending'
          ? candidate.status
          : 'pending',
      detail: typeof candidate.detail === 'string' ? candidate.detail : null,
      updatedAt: normalizeDateTimeValue(typeof candidate.updatedAt === 'string' ? candidate.updatedAt : null),
    });
  });

  return TEST_SERVER_DEPLOYMENT_STEP_KEYS.map((key) => normalizedByKey.get(key) ?? fallback.find((item) => item.key === key)!);
}

function normalizeTestServerDeploymentSnapshot(value: unknown): TestServerDeploymentSnapshot {
  if (!value || typeof value !== 'object') {
    return buildEmptyTestServerDeploymentSnapshot();
  }

  const candidate = value as TestServerDeploymentStateFilePayload;

  return {
    status: normalizeTestServerDeploymentStatus(candidate.status),
    phase: normalizeTestServerDeploymentPhase(candidate.phase),
    summary: typeof candidate.summary === 'string' ? candidate.summary : null,
    startedAt: normalizeDateTimeValue(typeof candidate.startedAt === 'string' ? candidate.startedAt : null),
    updatedAt: normalizeDateTimeValue(typeof candidate.updatedAt === 'string' ? candidate.updatedAt : null),
    completedAt: normalizeDateTimeValue(typeof candidate.completedAt === 'string' ? candidate.completedAt : null),
    lastError: typeof candidate.lastError === 'string' ? candidate.lastError : null,
    logExcerpt: typeof candidate.logExcerpt === 'string' ? candidate.logExcerpt : null,
    steps: normalizeTestServerDeploymentSteps(candidate.steps),
  };
}

export async function readTestServerDeploymentState(): Promise<TestServerDeploymentSnapshot | null> {
  try {
    const raw = await readFile(getTestServerDeploymentStatePath(), 'utf8');
    const snapshot = normalizeTestServerDeploymentSnapshot(JSON.parse(raw));
    return await resolveStaleRunningTestDeployment(snapshot);
  } catch {
    return null;
  }
}

async function writeTestServerDeploymentState(snapshot: TestServerDeploymentSnapshot) {
  const statePath = getTestServerDeploymentStatePath();
  await mkdir(path.dirname(statePath), { recursive: true });
  await writeFile(statePath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
}

async function clearTestServerDeploymentState() {
  await rm(getTestServerDeploymentStatePath(), { force: true }).catch(() => undefined);
}

function buildStaleTestServerDeploymentFailure(snapshot: TestServerDeploymentSnapshot) {
  const stalledAt = snapshot.updatedAt ?? snapshot.startedAt;
  const stalledLabel = stalledAt ? `마지막 상태 갱신 ${stalledAt}` : '상태 갱신 시각 확인 불가';
  return trimPreview(`TEST 배포 상태가 오래 갱신되지 않았고 잠금 파일도 없어 중단된 배포로 처리했습니다. ${stalledLabel}`, 500)
    ?? 'TEST 배포 상태가 오래 갱신되지 않아 중단된 배포로 처리했습니다.';
}

async function finalizeStaleRunningTestDeployment(snapshot: TestServerDeploymentSnapshot) {
  const failureMessage = buildStaleTestServerDeploymentFailure(snapshot);
  const now = new Date().toISOString();
  const activeStep = snapshot.steps.find((step) => step.status === 'running')?.key;

  snapshot.status = 'failed';
  snapshot.phase = 'failed';
  snapshot.summary = buildTestServerDeploymentSummary('failed');
  snapshot.completedAt = now;
  snapshot.updatedAt = now;
  snapshot.lastError = failureMessage;

  if (activeStep) {
    updateTestServerDeploymentStep(snapshot, activeStep, 'failed', failureMessage);
  }

  await writeTestServerDeploymentState(snapshot);
  return snapshot;
}

async function resolveStaleRunningTestDeployment(snapshot: TestServerDeploymentSnapshot) {
  if (snapshot.status !== 'running') {
    return snapshot;
  }

  const freshnessSource = snapshot.updatedAt ?? snapshot.startedAt;
  if (!freshnessSource) {
    return snapshot;
  }

  const staleForMs = Date.now() - Date.parse(freshnessSource);
  if (!Number.isFinite(staleForMs) || staleForMs < TEST_SERVER_DEPLOYMENT_LOCK_STALE_MS) {
    return snapshot;
  }

  const lockPath = getTestServerDeploymentLockPath();
  const lockStat = await stat(lockPath).catch(() => null);

  if (lockStat?.isFile()) {
    const lockFreshnessSource = normalizeDateTimeValue(lockStat.mtime.toISOString() ?? null);
    if (lockFreshnessSource && Date.now() - Date.parse(lockFreshnessSource) < TEST_SERVER_DEPLOYMENT_LOCK_STALE_MS) {
      return snapshot;
    }
  }

  await rm(lockPath, { force: true }).catch(() => undefined);
  return finalizeStaleRunningTestDeployment(snapshot);
}

async function acquireTestServerDeploymentLock() {
  const lockPath = getTestServerDeploymentLockPath();
  await mkdir(path.dirname(lockPath), { recursive: true });
  const startedAt = new Date().toISOString();

  try {
    const handle = await open(lockPath, 'wx');

    try {
      await handle.writeFile(JSON.stringify({ startedAt, key: 'test', pid: process.pid } satisfies RestartLockPayload) + '\n', 'utf8');
    } finally {
      await handle.close();
    }

    return lockPath;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
      throw error;
    }

    let existingStartedAt: string | null = null;

    try {
      const raw = await readFile(lockPath, 'utf8');
      const parsed = JSON.parse(raw) as Partial<RestartLockPayload>;
      existingStartedAt = normalizeDateTimeValue(typeof parsed.startedAt === 'string' ? parsed.startedAt : null);
      const lockStat = await stat(lockPath).catch(() => null);
      const freshnessSource = existingStartedAt ?? normalizeDateTimeValue(lockStat?.mtime.toISOString() ?? null);

      if (!freshnessSource || Date.now() - Date.parse(freshnessSource) > TEST_SERVER_DEPLOYMENT_LOCK_STALE_MS) {
        await rm(lockPath, { force: true }).catch(() => undefined);
        return acquireTestServerDeploymentLock();
      }
    } catch {
      // ignore read failures and keep conflict response below
    }

    const conflictError = new Error(
      existingStartedAt
        ? `TEST 배포가 이미 진행 중입니다. 시작 시각 ${existingStartedAt}`
        : 'TEST 배포가 이미 진행 중입니다.',
    );
    (conflictError as Error & { statusCode?: number }).statusCode = 409;
    throw conflictError;
  }
}

function buildTestServerDeploymentSummary(phase: TestServerDeploymentPhase) {
  switch (phase) {
    case 'commit-main-worktree':
      return 'main 작업트리 커밋 진행 중';
    case 'push-origin-main':
      return 'origin/main 푸시 진행 중';
    case 'build-test-app':
      return '테스트 앱 빌드 진행 중';
    case 'deploy-test-server':
      return '테스트 서버 배포 진행 중';
    case 'completed':
      return 'origin/main 푸시, 테스트 빌드, 테스트 배포가 완료되었습니다.';
    case 'failed':
      return 'TEST 배포에 실패했습니다.';
    default:
      return '테스트 배포 준비 중';
  }
}

function buildTestDeploymentFailureMessage(
  snapshot: Pick<TestServerDeploymentSnapshot, 'logExcerpt'>,
  error: unknown,
) {
  const failure = error instanceof Error ? (error as Error & { code?: number | string; signal?: string | null }) : null;
  const exitInfo = [
    failure?.code != null ? `exit:${String(failure.code)}` : null,
    failure?.signal ? `signal:${String(failure.signal)}` : null,
  ].filter(Boolean).join(' ');
  const logLines = (snapshot.logExcerpt ?? '')
    .split('\n')
    .map((line) => line.trim())
    .filter(Boolean);
  const lastMeaningfulLog = logLines.length > 0 ? logLines[logLines.length - 1] : null;

  return trimPreview([
    lastMeaningfulLog && lastMeaningfulLog !== failure?.message ? lastMeaningfulLog : null,
    failure?.message || null,
    exitInfo || null,
  ].filter(Boolean).join(' | '), 500) ?? 'TEST 배포에 실패했습니다.';
}

function appendTestServerDeploymentLog(previous: string | null, chunk: string) {
  const normalizedChunk = chunk.trim();

  if (!normalizedChunk) {
    return previous;
  }

  const combined = [previous, normalizedChunk].filter(Boolean).join('\n');
  return combined.length > TEST_SERVER_DEPLOYMENT_LOG_LIMIT
    ? combined.slice(combined.length - TEST_SERVER_DEPLOYMENT_LOG_LIMIT)
    : combined;
}

function updateTestServerDeploymentStep(
  snapshot: TestServerDeploymentSnapshot,
  key: TestServerDeploymentStepKey,
  status: TestServerDeploymentStepStatus,
  detail?: string | null,
) {
  const now = new Date().toISOString();
  snapshot.steps = snapshot.steps.map((step) => {
    if (step.key !== key) {
      return step;
    }

    return {
      ...step,
      status,
      detail: detail === undefined ? step.detail : detail,
      updatedAt: now,
    };
  });
  snapshot.updatedAt = now;
}

function markPreviousRunningStepCompleted(snapshot: TestServerDeploymentSnapshot, nextKey: TestServerDeploymentStepKey) {
  const previousRunning = snapshot.steps.find((step) => step.status === 'running' && step.key !== nextKey);
  if (previousRunning) {
    updateTestServerDeploymentStep(snapshot, previousRunning.key, 'completed');
  }
}

function scheduleTestServerDeploymentCleanup(completedAt: string) {
  const timer = setTimeout(() => {
    void (async () => {
      const snapshot = await readTestServerDeploymentState();
      if (snapshot?.status === 'completed' && snapshot.completedAt === completedAt) {
        await clearTestServerDeploymentState();
      }
    })();
  }, TEST_SERVER_DEPLOYMENT_CLEANUP_DELAY_MS);

  if (typeof timer === 'object' && timer && 'unref' in timer && typeof timer.unref === 'function') {
    timer.unref();
  }
}

async function runTestServerDeployment(
  lockPath: string,
  snapshot: TestServerDeploymentSnapshot,
  persist: () => Promise<void>,
) {
  const mainProjectRoot = resolveMainProjectRoot();
  const deployScript = path.join(mainProjectRoot, 'etc', 'commands', 'server-command', 'deploy-test.sh');

  const moveToStep = (key: TestServerDeploymentStepKey) => {
    markPreviousRunningStepCompleted(snapshot, key);
    snapshot.phase = key;
    snapshot.summary = buildTestServerDeploymentSummary(key);
    updateTestServerDeploymentStep(snapshot, key, 'running');
    void persist();
  };

  const appendOutput = (line: string) => {
    snapshot.logExcerpt = appendTestServerDeploymentLog(snapshot.logExcerpt, line);
    snapshot.updatedAt = new Date().toISOString();
    void persist();
  };

  const fail = async (message: string) => {
    const activeStep = snapshot.steps.find((step) => step.status === 'running')?.key ?? 'commit-main-worktree';
    snapshot.status = 'failed';
    snapshot.phase = 'failed';
    snapshot.summary = buildTestServerDeploymentSummary('failed');
    snapshot.lastError = message;
    snapshot.updatedAt = new Date().toISOString();
    updateTestServerDeploymentStep(snapshot, activeStep, 'failed', message);
    await persist();
  };

  try {
    await new Promise<void>((resolve, reject) => {
      const child = spawn('sh', [deployScript], {
        cwd: mainProjectRoot,
        env: {
          ...process.env,
          MAIN_PROJECT_ROOT: mainProjectRoot,
          REPO_ROOT: mainProjectRoot,
        },
        stdio: ['ignore', 'pipe', 'pipe'],
      });

      let stdoutBuffer = '';
      let stderrBuffer = '';

      const processLine = (line: string) => {
        const trimmed = line.trim();
        const marker = trimmed.match(/^::step::([a-z-]+)$/);

        if (marker) {
          const nextStep = normalizeTestServerDeploymentStepKey(marker[1]);
          if (nextStep) {
            moveToStep(nextStep);
          }
          return;
        }

        appendOutput(line);
      };

      const flushBufferedLines = (buffer: string) => {
        const normalized = buffer.replace(/\r$/, '').trim();
        if (normalized) {
          processLine(normalized);
        }
      };

      const attachReader = (stream: NodeJS.ReadableStream | null, target: 'stdout' | 'stderr') => {
        if (!stream) {
          return;
        }

        stream.setEncoding('utf8');
        stream.on('data', (chunk: string) => {
          if (target === 'stdout') {
            stdoutBuffer += chunk;
            while (stdoutBuffer.includes('\n')) {
              const index = stdoutBuffer.indexOf('\n');
              const line = stdoutBuffer.slice(0, index).replace(/\r$/, '');
              stdoutBuffer = stdoutBuffer.slice(index + 1);
              processLine(line);
            }
            return;
          }

          stderrBuffer += chunk;
          while (stderrBuffer.includes('\n')) {
            const index = stderrBuffer.indexOf('\n');
            const line = stderrBuffer.slice(0, index).replace(/\r$/, '');
            stderrBuffer = stderrBuffer.slice(index + 1);
            processLine(line);
          }
        });
      };

      attachReader(child.stdout, 'stdout');
      attachReader(child.stderr, 'stderr');

      child.once('error', reject);
      child.once('close', (code, signal) => {
        flushBufferedLines(stdoutBuffer);
        flushBufferedLines(stderrBuffer);

        if (code === 0) {
          resolve();
          return;
        }

        reject(Object.assign(new Error(`deploy-test exited with ${code ?? 'unknown'}${signal ? ` (${signal})` : ''}`), {
          code,
          signal,
        }));
      });
    });

    const activeStep = snapshot.steps.find((step) => step.status === 'running')?.key;
    if (activeStep) {
      updateTestServerDeploymentStep(snapshot, activeStep, 'completed');
    }
    const completedAt = new Date().toISOString();
    snapshot.status = 'completed';
    snapshot.phase = 'completed';
    snapshot.summary = buildTestServerDeploymentSummary('completed');
    snapshot.completedAt = completedAt;
    snapshot.updatedAt = completedAt;
    snapshot.lastError = null;
    await persist();
    scheduleTestServerDeploymentCleanup(completedAt);
  } catch (error) {
    const message = buildTestDeploymentFailureMessage(snapshot, error);
    await fail(message);
  } finally {
    await rm(lockPath, { force: true }).catch(() => undefined);
  }
}

export async function startTestServerDeployment() {
  const lockPath = await acquireTestServerDeploymentLock();
  const startedAt = new Date().toISOString();
  const snapshot = buildEmptyTestServerDeploymentSnapshot();
  snapshot.status = 'running';
  snapshot.phase = 'commit-main-worktree';
  snapshot.summary = buildTestServerDeploymentSummary('commit-main-worktree');
  snapshot.startedAt = startedAt;
  snapshot.updatedAt = startedAt;
  updateTestServerDeploymentStep(snapshot, 'commit-main-worktree', 'running', 'main 작업트리 변경을 커밋합니다.');
  await writeTestServerDeploymentState(snapshot);

  let persistQueue = Promise.resolve();
  const persist = async () => {
    persistQueue = persistQueue.then(() => writeTestServerDeploymentState(snapshot)).catch(() => undefined);
    await persistQueue;
  };

  void runTestServerDeployment(lockPath, snapshot, persist);
  return snapshot;
}
