import os from "node:os";
import path from "node:path";
import { backup, readIfExists, removeFileAndPrune, sha256, writeAtomic } from "./fsx.ts";
import { removeBlock, upsertBlock } from "./marker.ts";
import { dropInstall, findInstall, inspectDrift, readManifest, upsertInstall } from "./manifest.ts";
import { ADAPTERS, getAdapter } from "./targets/index.ts";
import type {
  InstalledFile,
  InstallPlan,
  InstallRecord,
  Scope,
  SkillSource,
  TargetContext,
  TargetId,
} from "./types.ts";

export interface EnvOptions {
  home?: string;
  cwd?: string;
}

function ctxOf(scope: Scope, skill: SkillSource, env: EnvOptions = {}): TargetContext {
  return {
    scope,
    home: env.home ?? os.homedir(),
    cwd: env.cwd ?? process.cwd(),
    skill,
  };
}

/** 시스템에 흔적이 있는 타깃을 골라준다 — TUI 의 기본 체크 상태로 쓴다 */
export async function detectTargets(skill: SkillSource, env: EnvOptions = {}): Promise<TargetId[]> {
  const found: TargetId[] = [];
  for (const a of ADAPTERS) {
    const scope: Scope = a.scopes.includes("user") ? "user" : "project";
    if (await a.detect(ctxOf(scope, skill, env))) found.push(a.id);
  }
  return found;
}

export async function planInstall(
  target: TargetId,
  scope: Scope,
  skill: SkillSource,
  env: EnvOptions = {},
): Promise<InstallPlan> {
  const adapter = getAdapter(target);
  if (!adapter.scopes.includes(scope)) {
    const only = adapter.scopes.join("/");
    return {
      target,
      scope,
      root: "",
      actions: [],
      alreadyInstalled: false,
      blocked: `${adapter.label} 은(는) ${only} 범위만 지원한다`,
    };
  }
  return adapter.plan(ctxOf(scope, skill, env));
}

export interface ApplyResult {
  record: InstallRecord;
  written: string[];
  backedUp: string[];
  /** 사용자가 고쳐서 건너뛴 파일 */
  skipped: string[];
}

/**
 * 계획을 실제로 디스크에 반영한다.
 *
 * 업그레이드 안전장치: 이전 설치 기록이 있으면 각 파일의 해시를 비교해서,
 * 사용자가 손댄 파일은 기본적으로 건드리지 않는다(force 로만 덮고, 덮을 때도 .orig 로 남긴다).
 */
export async function applyPlan(
  plan: InstallPlan,
  version: string,
  opts: { force?: boolean; env?: EnvOptions } = {},
): Promise<ApplyResult> {
  if (plan.blocked) throw new Error(plan.blocked);
  const home = opts.env?.home;

  const prev = findInstall(await readManifest(home), plan.target, plan.scope, plan.root);
  const modified = new Set<string>();
  if (prev) {
    const drift = await inspectDrift(prev);
    for (const p of drift.modified) modified.add(p);
  }

  const files: InstalledFile[] = [];
  const written: string[] = [];
  const backedUp: string[] = [];
  const skipped: string[] = [];

  for (const action of plan.actions) {
    const isDirty = modified.has(action.path);
    if (isDirty && !opts.force) {
      // 건너뛰더라도 매니페스트에서 빠지면 uninstall 이 이 파일을 놓친다 — 기록은 남긴다.
      // 이때 "현재" 해시를 쓰면 안 된다. 그러면 다음 update 에서 드리프트가 사라져
      // 사용자가 고친 파일을 조용히 덮어쓰게 된다. 설치 당시 해시를 그대로 유지한다.
      const kept = prev?.files.find((f) => f.path === action.path);
      files.push(
        kept ?? {
          path: action.path,
          sha256: sha256(action.kind === "inject" ? action.content.trim() : action.content),
          ...(action.kind === "inject" ? { marker: action.marker } : {}),
        },
      );
      skipped.push(action.path);
      continue;
    }
    if (isDirty && opts.force) {
      const b = await backup(action.path);
      if (b) backedUp.push(b);
    }

    if (action.kind === "write") {
      await writeAtomic(action.path, action.content);
      files.push({ path: action.path, sha256: sha256(action.content) });
    } else {
      const cur = (await readIfExists(action.path)) ?? "";
      const next = upsertBlock(cur, action.marker, action.content);
      await writeAtomic(action.path, next);
      // 마커 방식은 블록 안쪽만 해시한다 — 문서의 다른 부분은 사용자 자유다
      files.push({ path: action.path, sha256: sha256(action.content.trim()), marker: action.marker });
    }
    written.push(action.path);
  }

  const record: InstallRecord = {
    target: plan.target,
    scope: plan.scope,
    root: plan.root,
    version,
    installedAt: new Date().toISOString(),
    files,
  };
  await upsertInstall(record, home);

  return { record, written, backedUp, skipped };
}

export interface RemoveResult {
  removed: string[];
  keptModified: string[];
}

/** 매니페스트에 기록된 것만 되돌린다. 기록에 없는 파일은 손대지 않는다. */
export async function removeInstall(
  record: InstallRecord,
  opts: { force?: boolean; env?: EnvOptions } = {},
): Promise<RemoveResult> {
  const drift = await inspectDrift(record);
  const dirty = new Set(drift.modified);
  const removed: string[] = [];
  const keptModified: string[] = [];

  for (const f of record.files) {
    if (dirty.has(f.path) && !opts.force) {
      keptModified.push(f.path);
      continue;
    }
    if (f.marker) {
      const cur = await readIfExists(f.path);
      if (cur === null) continue;
      const next = removeBlock(cur, f.marker);
      // 블록만 남아 있던 문서라면 파일째 지운다
      if (next.trim().length === 0) await removeFileAndPrune(f.path, path.dirname(f.path));
      else await writeAtomic(f.path, next);
    } else {
      await removeFileAndPrune(f.path, record.root);
    }
    removed.push(f.path);
  }

  if (keptModified.length === 0) {
    await dropInstall(record.target, record.scope, record.root, opts.env?.home);
  } else {
    // 일부만 남았으면 기록도 남은 것만 유지한다
    await upsertInstall({ ...record, files: record.files.filter((f) => dirty.has(f.path)) }, opts.env?.home);
  }

  return { removed, keptModified };
}
