/**
 * VersionStore port(契约 C1)+ 开发用内存实现。
 *
 * 版本团队交付 npm 工具包后,用同一接口替换 createInMemoryVersionStore。
 * 语义红线(计划"Mock 保真红线"):冲突走返回值(S1)、baseVersion=null 表新建(S2)、
 * init 幂等(S7)、同 projectDir 写操作串行化(S4)、文件级版本(D16)。
 *
 * 持久化:传 persistPath 时把仓库快照落盘(JSON),重启后恢复 —— 纯粹为开发期
 * demo 稳定,不改变任何语义;生产由真包替换,此文件不参与。
 */

import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import path from "node:path";
import { FileLibError } from "./model.js";

export type VersionId = string;

export interface CommitRequest {
  /** 编辑起始版本;null 表示新建文件(已存在则 conflict,S2)。 */
  readonly baseVersion: VersionId | null;
  readonly content: string | Buffer;
  readonly message?: string | undefined;
  readonly author?: string | undefined;
}

export type CommitResult =
  | { readonly status: "ok"; readonly version: VersionId }
  | { readonly status: "conflict"; readonly currentVersion: VersionId };

export interface VersionInfo {
  readonly version: VersionId;
  readonly message: string;
  readonly author: string | undefined;
  readonly committedAt: string;
}

export interface FileEntry {
  readonly path: string;
  readonly size: number;
}

export interface VersionStore {
  init(projectDir: string): Promise<void>;
  list(projectDir: string, prefix?: string): Promise<FileEntry[]>;
  head(projectDir: string, filePath: string): Promise<VersionId>;
  read(projectDir: string, filePath: string, at?: VersionId): Promise<Buffer>;
  commit(projectDir: string, filePath: string, req: CommitRequest): Promise<CommitResult>;
  remove(projectDir: string, filePath: string, baseVersion: VersionId): Promise<CommitResult>;
  diff(projectDir: string, filePath: string, from: VersionId, to: VersionId): Promise<string>;
  history(projectDir: string, filePath: string, limit?: number): Promise<VersionInfo[]>;
}

interface StoredVersion {
  readonly version: VersionId;
  readonly content: Buffer;
  readonly message: string;
  readonly author: string | undefined;
  readonly committedAt: string;
  readonly deleted: boolean;
}

interface Repo {
  /** 每文件一条版本链(D16:版本是文件级的,互不干扰)。 */
  readonly files: Map<string, StoredVersion[]>;
  counter: number;
}

/** S4:同 projectDir 的写操作经 per-repo promise 链串行化。 */
function createKeySerializer(): <T>(key: string, fn: () => Promise<T>) => Promise<T> {
  const tails = new Map<string, Promise<unknown>>();
  return <T>(key: string, fn: () => Promise<T>): Promise<T> => {
    const prev = tails.get(key) ?? Promise.resolve();
    const next = prev.then(fn, fn);
    tails.set(key, next.catch(() => undefined));
    return next;
  };
}

function toBuffer(content: string | Buffer): Buffer {
  return typeof content === "string" ? Buffer.from(content, "utf8") : content;
}

/** 极简 unified-diff(mock 保真够用;真包的 diff 以版本团队为准)。 */
function naiveDiff(fromText: string, toText: string): string {
  const a = fromText.split("\n");
  const b = toText.split("\n");
  const out: string[] = ["--- a", "+++ b"];
  const max = Math.max(a.length, b.length);
  for (let i = 0; i < max; i += 1) {
    const al = a[i];
    const bl = b[i];
    if (al === bl) {
      if (al !== undefined) out.push(` ${al}`);
    } else {
      if (al !== undefined) out.push(`-${al}`);
      if (bl !== undefined) out.push(`+${bl}`);
    }
  }
  return out.join("\n");
}

export function createInMemoryVersionStore(persistPath?: string): VersionStore {
  const repos = new Map<string, Repo>();
  const serialize = createKeySerializer();

  /* ---------------- 开发期快照持久化(语义不变,仅防重启丢失) ---------------- */
  interface PersistedVersion extends Omit<StoredVersion, "content"> {
    content: string; // base64
  }
  function loadPersisted(): void {
    if (persistPath === undefined) return;
    try {
      const raw = JSON.parse(readFileSync(persistPath, "utf8")) as {
        repos: Record<string, { counter: number; files: Record<string, PersistedVersion[]> }>;
      };
      for (const [dir, repo] of Object.entries(raw.repos)) {
        const files = new Map<string, StoredVersion[]>();
        for (const [filePath, versions] of Object.entries(repo.files)) {
          files.set(filePath, versions.map((v) => ({ ...v, content: Buffer.from(v.content, "base64") })));
        }
        repos.set(dir, { files, counter: repo.counter });
      }
    } catch { /* 无快照或损坏 → 空库起步(开发语义) */ }
  }
  function savePersisted(): void {
    if (persistPath === undefined) return;
    const out: Record<string, { counter: number; files: Record<string, PersistedVersion[]> }> = {};
    for (const [dir, repo] of repos) {
      const files: Record<string, PersistedVersion[]> = {};
      for (const [filePath, versions] of repo.files) {
        files[filePath] = versions.map((v) => ({ ...v, content: v.content.toString("base64") }));
      }
      out[dir] = { counter: repo.counter, files };
    }
    try {
      mkdirSync(path.dirname(persistPath), { recursive: true });
      const tmp = `${persistPath}.tmp`;
      writeFileSync(tmp, JSON.stringify({ repos: out }), "utf8");
      renameSync(tmp, persistPath);
    } catch { /* 快照失败不影响开发使用 */ }
  }
  loadPersisted();

  function requireRepo(projectDir: string): Repo {
    const repo = repos.get(projectDir);
    if (repo === undefined) {
      throw new FileLibError(404, "repo_not_found", `repository not initialized: ${projectDir}`);
    }
    return repo;
  }

  function liveVersion(repo: Repo, filePath: string): StoredVersion {
    const chain = repo.files.get(filePath);
    const latest = chain?.[chain.length - 1];
    if (chain === undefined || latest === undefined || latest.deleted) {
      throw new FileLibError(404, "file_not_found", `file not found: ${filePath}`);
    }
    return latest;
  }

  function findVersion(repo: Repo, filePath: string, version: VersionId): StoredVersion {
    const found = repo.files.get(filePath)?.find((v) => v.version === version);
    if (found === undefined) {
      throw new FileLibError(404, "version_not_found", `version not found: ${filePath}@${version}`);
    }
    return found;
  }

  return {
    async init(projectDir) {
      await serialize(projectDir, async () => {
        // S7:幂等,重复调用不报错、不重建。
        const created = repos.get(projectDir) === undefined;
        repos.set(projectDir, repos.get(projectDir) ?? { files: new Map(), counter: 0 });
        if (created) savePersisted();
      });
    },

    async list(projectDir, prefix) {
      const repo = requireRepo(projectDir);
      const out: FileEntry[] = [];
      for (const [path, chain] of repo.files) {
        const latest = chain[chain.length - 1];
        if (latest === undefined || latest.deleted) continue;
        if (prefix !== undefined && !path.startsWith(prefix)) continue;
        out.push({ path, size: latest.content.byteLength });
      }
      return out.sort((a, b) => a.path.localeCompare(b.path));
    },

    async head(projectDir, filePath) {
      return liveVersion(requireRepo(projectDir), filePath).version;
    },

    async read(projectDir, filePath, at) {
      const repo = requireRepo(projectDir);
      if (at !== undefined) return findVersion(repo, filePath, at).content;
      return liveVersion(repo, filePath).content;
    },

    async commit(projectDir, filePath, req) {
      return serialize(projectDir, async (): Promise<CommitResult> => {
        const repo = requireRepo(projectDir);
        const chain = repo.files.get(filePath) ?? [];
        const latest = chain[chain.length - 1];
        const currentVersion = latest !== undefined && !latest.deleted ? latest.version : null;

        // S2:新建(baseVersion null)要求文件当前不存在;否则要求 baseVersion 精确等于当前版本(S1)。
        if (req.baseVersion === null) {
          if (currentVersion !== null) return { status: "conflict", currentVersion };
        } else if (req.baseVersion !== currentVersion) {
          return {
            status: "conflict",
            currentVersion: currentVersion ?? req.baseVersion,
          };
        }

        repo.counter += 1;
        const version = `v${repo.counter}`;
        chain.push({
          version,
          content: toBuffer(req.content),
          message: req.message ?? `commit ${version}`,
          author: req.author,
          committedAt: new Date().toISOString(),
          deleted: false,
        });
        repo.files.set(filePath, chain);
        savePersisted();
        return { status: "ok", version };
      });
    },

    async remove(projectDir, filePath, baseVersion) {
      return serialize(projectDir, async (): Promise<CommitResult> => {
        const repo = requireRepo(projectDir);
        const chain = repo.files.get(filePath) ?? [];
        const latest = chain[chain.length - 1];
        const currentVersion = latest !== undefined && !latest.deleted ? latest.version : null;
        if (currentVersion === null) {
          throw new FileLibError(404, "file_not_found", `file not found: ${filePath}`);
        }
        if (baseVersion !== currentVersion) return { status: "conflict", currentVersion };
        repo.counter += 1;
        const version = `v${repo.counter}`;
        chain.push({
          version,
          content: Buffer.alloc(0),
          message: `remove ${filePath}`,
          author: undefined,
          committedAt: new Date().toISOString(),
          deleted: true,
        });
        repo.files.set(filePath, chain);
        savePersisted();
        return { status: "ok", version };
      });
    },

    async diff(projectDir, filePath, from, to) {
      const repo = requireRepo(projectDir);
      const a = findVersion(repo, filePath, from);
      const b = findVersion(repo, filePath, to);
      return naiveDiff(a.content.toString("utf8"), b.content.toString("utf8"));
    },

    async history(projectDir, filePath, limit) {
      const repo = requireRepo(projectDir);
      const chain = repo.files.get(filePath) ?? [];
      const infos: VersionInfo[] = chain.map((v) => ({
        version: v.version,
        message: v.message,
        author: v.author,
        committedAt: v.committedAt,
      }));
      const ordered = infos.reverse();
      return limit !== undefined ? ordered.slice(0, limit) : ordered;
    },
  };
}
