import { join } from 'node:path';
import { mkdir } from 'node:fs/promises';
import type { ExportService } from './ExportService.js';
import type { ImportService } from './ImportService.js';
import { GitClient } from './GitClient.js';

/**
 * Cut E final review fix: 'noteId' was misleading — F5 export filenames are
 * `<date>-<id8>-<slug>.md` (composeFilename), not `<uuid>.md`. The git checkout /
 * resolve operations use the FULL relative path (e.g., `notes/2026-05-09-abc12345-회의.md`).
 * `path` matches what we actually pass to `git checkout --ours/theirs`.
 */
export interface SyncConflict {
  path: string;
  localText: string;
  remoteText: string;
}

export interface SyncStatus {
  ok: boolean;
  reason?: string;
  changed?: boolean;
  localSha?: string | null;
  pushed?: boolean;
  importedCount?: number;
  conflicts?: SyncConflict[];
}

export class SyncService {
  private syncDir: string;
  private lastConflicts: SyncConflict[] = [];
  private lastResult: SyncStatus | null = null;
  private lastAt: string | null = null;

  constructor(
    private profileDir: string,
    private exportSvc: ExportService,
    private importSvc: ImportService,
    private now: () => Date = () => new Date()
  ) {
    this.syncDir = join(profileDir, 'sync');
  }

  getSyncDir(): string {
    return this.syncDir;
  }

  async isConfigured(): Promise<boolean> {
    const git = new GitClient(this.syncDir);
    if (!(await git.isRepo())) return false;
    if (!(await git.hasRemote())) return false;
    return true;
  }

  getLastStatus(): { lastAt: string | null; lastResult: SyncStatus | null } {
    return { lastAt: this.lastAt, lastResult: this.lastResult };
  }

  listConflicts(): SyncConflict[] {
    return this.lastConflicts;
  }

  async sync(): Promise<SyncStatus> {
    const result = await this.runSync();
    this.lastResult = result;
    this.lastAt = this.now().toISOString();
    if (result.reason === 'conflict' && result.conflicts) {
      this.lastConflicts = result.conflicts;
    } else if (result.ok) {
      this.lastConflicts = [];
    }
    return result;
  }

  /**
   * v0.3.0 Cut E — conflict 해결. local/remote 2 choice (both deferred to v0.3.1+).
   * 사용자가 ConflictModal 에서 선택 → IPC → 본 메서드. 각 conflict 의 path 별 호출.
   *
   * - 'local' = 내 것 사용 (origin 변경 폐기) → git checkout --ours
   * - 'remote' = 원격 사용 → git checkout --theirs + applySyncFromDir (local DB 갱신)
   *
   * 모든 conflict 해결 후 rebase --continue 가 성공 → push.
   * UI 가 여러 conflict 를 loop 호출하면 마지막 호출에서 push 까지 완료.
   *
   * Cut E final review fix: 파라미터를 path 로 변경 (옛 noteId 는 export filename slug,
   * UUID 아님 — 혼동 회피).
   */
  async resolveConflict(
    path: string,
    choice: 'local' | 'remote'
  ): Promise<{ ok: true } | { ok: false; reason: string }> {
    const git = new GitClient(this.syncDir);
    const flag = choice === 'local' ? '--ours' : '--theirs';

    const checkout = await git.run(['checkout', flag, path]);
    if (checkout.exitCode !== 0) {
      return { ok: false, reason: `checkout failed: ${checkout.stderr}` };
    }

    await git.addAll();

    const cont = await git.run(['rebase', '--continue']);
    if (cont.exitCode !== 0) {
      // Likely other unresolved files — UI will call resolveConflict for them.
      return { ok: false, reason: `rebase --continue failed: ${cont.stderr}` };
    }

    if (choice === 'remote') {
      try {
        await this.importSvc.applySyncFromDir(this.syncDir);
      } catch (e) {
        return { ok: false, reason: `re-import failed: ${(e as Error).message}` };
      }
    }

    try {
      await git.push();
    } catch (e) {
      return { ok: false, reason: `push failed: ${(e as Error).message}` };
    }

    // Remove this path from cached conflicts list
    this.lastConflicts = this.lastConflicts.filter((c) => c.path !== path);

    return { ok: true };
  }

  private async runSync(): Promise<SyncStatus> {
    if (!(await this.isConfigured())) return { ok: false, reason: 'not_configured' };

    const git = new GitClient(this.syncDir);

    // 1. local export
    try {
      await mkdir(this.syncDir, { recursive: true });
      await this.exportSvc.export(this.syncDir, { includeMedia: true });
    } catch (e) {
      return { ok: false, reason: `export failed: ${(e as Error).message}` };
    }

    // 2. local commit (only if changed)
    let localSha: string | null = null;
    let localChanged = false;
    try {
      await git.addAll();
      localChanged = await git.hasUncommittedChanges();
      if (localChanged) {
        const c = await git.commit(`chore(notes): sync ${this.now().toISOString()}`);
        localSha = c.sha;
      }
    } catch (e) {
      return { ok: false, reason: `local commit failed: ${(e as Error).message}` };
    }

    // 3. fetch
    const fetchR = await git.fetch();
    if (fetchR.exitCode !== 0) return { ok: false, reason: `fetch failed: ${fetchR.stderr}` };

    // 4. rebase — skip if origin/main doesn't exist yet (first-push, empty remote)
    const hasOriginMain = await git.refExists('origin/main');
    if (hasOriginMain) {
      const rebaseR = await git.rebaseOnto('origin/main');
      if (rebaseR.exitCode !== 0) {
        const files = await git.listConflicts();
        // Cut E final review fix — populate localText/remoteText from rebase index
        // BEFORE aborting. `git show :2:<path>` = ours (local during rebase),
        // `:3:<path>` = theirs (remote being applied). UI shows side-by-side diff.
        const conflicts: SyncConflict[] = [];
        for (const path of files) {
          const ours = await git.run(['show', `:2:${path}`]);
          const theirs = await git.run(['show', `:3:${path}`]);
          conflicts.push({
            path,
            localText: ours.exitCode === 0 ? ours.stdout : '',
            remoteText: theirs.exitCode === 0 ? theirs.stdout : ''
          });
        }
        await git.rebaseAbort();
        return { ok: false, reason: 'conflict', conflicts };
      }
    }

    // 5. re-import
    let importedCount = 0;
    try {
      const r = await this.importSvc.applySyncFromDir(this.syncDir);
      importedCount = r.changedCount;
    } catch (e) {
      return { ok: false, reason: `re-import failed: ${(e as Error).message}` };
    }

    // 6. push
    try {
      await git.push();
    } catch (e) {
      return { ok: false, reason: `push failed: ${(e as Error).message}` };
    }

    return { ok: true, changed: localChanged || importedCount > 0, localSha, importedCount, pushed: true };
  }

}
