import { createHash } from "node:crypto";
import type { OlpProjectModel } from "../olp/index.js";
import { createOlpProject, validateOlpProject } from "../olp/index.js";

export interface ProjectManifest {
  schemaVersion: "workspace/0.1";
  projectId: string;
  projectName: string;
  revision?: string;
  createdAt: string;
  updatedAt: string;
  layout: {
    root: string;
    projectJson: string;
    programsDir: string;
    reportsDir: string;
    snapshotsDir: string;
  };
  files: WorkspaceFileEntry[];
  migrations: string[];
}

export interface WorkspaceFileEntry {
  path: string;
  kind: "json" | "text" | "binary";
  sha256: string;
  bytes: number;
}

export interface WorkspaceSnapshot {
  id: string;
  manifest: ProjectManifest;
  files: Record<string, string>;
}

export interface WorkspaceBundle {
  format: "kdl-workspace-bundle/0.1";
  manifest: ProjectManifest;
  files: Record<string, string>;
  checksum?: string;
}

export interface WorkspaceDamageReport {
  ok: boolean;
  missing: string[];
  checksumMismatch: string[];
  diagnostics: string[];
}

export interface WorkspaceStore {
  writeText(path: string, value: string): Promise<void>;
  readText(path: string): Promise<string>;
  delete(path: string): Promise<void>;
  list(prefix?: string): Promise<string[]>;
}

export class MemoryWorkspaceStore implements WorkspaceStore {
  private readonly files = new Map<string, string>();

  constructor(initialFiles: Record<string, string> = {}) {
    for (const [path, value] of Object.entries(initialFiles)) {
      this.files.set(normalizePath(path), value);
    }
  }

  async writeText(path: string, value: string): Promise<void> {
    this.files.set(normalizePath(path), value);
  }

  async readText(path: string): Promise<string> {
    const normalized = normalizePath(path);
    const value = this.files.get(normalized);
    if (value === undefined) {
      throw new Error(`Workspace file ${normalized} not found`);
    }
    return value;
  }

  async delete(path: string): Promise<void> {
    this.files.delete(normalizePath(path));
  }

  async list(prefix = ""): Promise<string[]> {
    const normalizedPrefix = normalizePath(prefix);
    return [...this.files.keys()]
      .filter((path) => normalizedPrefix === "" || path.startsWith(normalizedPrefix))
      .sort();
  }

  async exists(path: string): Promise<boolean> {
    return this.files.has(normalizePath(path));
  }

  dump(): Record<string, string> {
    return Object.fromEntries([...this.files.entries()].sort(([left], [right]) => left.localeCompare(right)));
  }
}

export const MemoryWorkspaceStorage = MemoryWorkspaceStore;

export type WorkspaceStorage = WorkspaceStore & {
  exists?: (path: string) => Promise<boolean>;
};

export interface CompatibilityProjectManifest {
  schemaVersion: 1;
  projectId: string;
  name: string;
  createdAt: string;
  updatedAt: string;
  directories: {
    model: string;
    programs: string;
    reports: string;
    imports: string;
    delivery: string;
    traces: string;
  };
  entrypoints: {
    model: string;
  };
}

export interface CompatibilityWorkspaceSnapshot {
  manifest: CompatibilityProjectManifest;
  files: Record<string, string>;
  checksum: string;
}

export interface CompatibilityWorkspaceDamage {
  code: "WORKSPACE_MANIFEST_MISSING" | "WORKSPACE_MANIFEST_INVALID" | "WORKSPACE_MODEL_MISSING" | "WORKSPACE_MODEL_INVALID" | "WORKSPACE_CHECKSUM_MISMATCH";
  message: string;
  path?: string;
}

export interface CompatibilityWorkspaceIntegrityReport {
  ok: boolean;
  damages: CompatibilityWorkspaceDamage[];
}

export interface CompatibilityWorkspaceBundle {
  format: "kdl-workspace-bundle";
  version: 1;
  manifest: CompatibilityProjectManifest;
  files: Record<string, string>;
  checksum: string;
}

const COMPAT_MANIFEST_PATH = "project.json";

export function createProjectManifest(projectId: string, name: string, now = new Date().toISOString()): CompatibilityProjectManifest {
  return {
    schemaVersion: 1,
    projectId,
    name,
    createdAt: now,
    updatedAt: now,
    directories: {
      model: "model",
      programs: "programs",
      reports: "reports",
      imports: "imports",
      delivery: "delivery",
      traces: "traces"
    },
    entrypoints: {
      model: "model/olp-project.json"
    }
  };
}

export async function initializeWorkspace(
  storage: WorkspaceStorage,
  options: { projectId: string; name: string; model?: OlpProjectModel; now?: string }
): Promise<CompatibilityProjectManifest> {
  const manifest = createProjectManifest(options.projectId, options.name, options.now);
  await writeJson(storage, COMPAT_MANIFEST_PATH, manifest);
  await writeJson(storage, manifest.entrypoints.model, options.model ?? createOlpProject({ id: options.projectId, name: options.name }));
  return manifest;
}

export async function readManifest(storage: WorkspaceStorage): Promise<CompatibilityProjectManifest> {
  return migrateManifest(await readJson<unknown>(storage, COMPAT_MANIFEST_PATH));
}

export async function readProjectModel(storage: WorkspaceStorage): Promise<OlpProjectModel> {
  const manifest = await readManifest(storage);
  return readJson<OlpProjectModel>(storage, manifest.entrypoints.model);
}

export async function writeProjectModel(storage: WorkspaceStorage, model: OlpProjectModel, now = new Date().toISOString()): Promise<void> {
  const manifest = await readManifest(storage);
  await writeJson(storage, manifest.entrypoints.model, model);
  await writeJson(storage, COMPAT_MANIFEST_PATH, { ...manifest, updatedAt: now });
}

export async function readJson<T>(storage: WorkspaceStorage, path: string): Promise<T> {
  return JSON.parse(await storage.readText(path)) as T;
}

export async function writeJson(storage: WorkspaceStorage, path: string, value: unknown): Promise<void> {
  await storage.writeText(path, `${stableStringify(value)}\n`);
}

export async function snapshotWorkspace(storage: WorkspaceStorage): Promise<CompatibilityWorkspaceSnapshot> {
  const files: Record<string, string> = {};
  for (const path of await storage.list()) {
    files[path] = await storage.readText(path);
  }
  return {
    manifest: migrateManifest(JSON.parse(files[COMPAT_MANIFEST_PATH] ?? "{}")),
    files,
    checksum: checksumFiles(files)
  };
}

export async function restoreWorkspace(storage: WorkspaceStorage, snapshot: CompatibilityWorkspaceSnapshot): Promise<void> {
  for (const path of await storage.list()) {
    await storage.delete(path);
  }
  for (const [path, value] of Object.entries(snapshot.files)) {
    await storage.writeText(path, value);
  }
}

export async function exportWorkspaceBundle(storage: WorkspaceStorage): Promise<string> {
  const snapshot = await snapshotWorkspace(storage);
  const bundle: CompatibilityWorkspaceBundle = {
    format: "kdl-workspace-bundle",
    version: 1,
    manifest: snapshot.manifest,
    files: snapshot.files,
    checksum: snapshot.checksum
  };
  return `${stableStringify(bundle)}\n`;
}

export async function importWorkspaceBundle(storage: WorkspaceStorage, text: string): Promise<CompatibilityWorkspaceIntegrityReport> {
  const bundle = JSON.parse(text) as CompatibilityWorkspaceBundle;
  if (bundle.format !== "kdl-workspace-bundle" || bundle.version !== 1) {
    throw new Error("Unsupported workspace bundle");
  }
  if (checksumFiles(bundle.files) !== bundle.checksum) {
    return {
      ok: false,
      damages: [{ code: "WORKSPACE_CHECKSUM_MISMATCH", message: "Bundle checksum does not match file contents" }]
    };
  }
  await restoreWorkspace(storage, {
    manifest: bundle.manifest,
    files: bundle.files,
    checksum: bundle.checksum
  });
  return detectWorkspaceDamage(storage, bundle.checksum);
}

export async function detectWorkspaceDamage(storage: WorkspaceStorage, expectedChecksum?: string): Promise<CompatibilityWorkspaceIntegrityReport> {
  const damages: CompatibilityWorkspaceDamage[] = [];
  const exists = storage.exists
    ? await storage.exists(COMPAT_MANIFEST_PATH)
    : (await storage.list()).includes(COMPAT_MANIFEST_PATH);
  if (!exists) {
    return {
      ok: false,
      damages: [{ code: "WORKSPACE_MANIFEST_MISSING", message: "project.json is missing", path: COMPAT_MANIFEST_PATH }]
    };
  }

  let manifest: CompatibilityProjectManifest | undefined;
  try {
    manifest = await readManifest(storage);
  } catch (cause) {
    damages.push({ code: "WORKSPACE_MANIFEST_INVALID", message: `project.json is invalid: ${String(cause)}`, path: COMPAT_MANIFEST_PATH });
  }

  if (manifest) {
    const modelExists = storage.exists
      ? await storage.exists(manifest.entrypoints.model)
      : (await storage.list()).includes(manifest.entrypoints.model);
    if (!modelExists) {
      damages.push({ code: "WORKSPACE_MODEL_MISSING", message: `Project model ${manifest.entrypoints.model} is missing`, path: manifest.entrypoints.model });
    } else {
      try {
        const model = await readJson<OlpProjectModel>(storage, manifest.entrypoints.model);
        const validation = validateOlpProject(model);
        for (const issue of validation.issues.filter((item) => item.severity === "error")) {
          damages.push({ code: "WORKSPACE_MODEL_INVALID", message: `${issue.code}: ${issue.message}`, path: manifest.entrypoints.model });
        }
      } catch (cause) {
        damages.push({ code: "WORKSPACE_MODEL_INVALID", message: `Project model is invalid: ${String(cause)}`, path: manifest.entrypoints.model });
      }
    }
  }

  if (expectedChecksum) {
    const snapshot = await snapshotWorkspace(storage);
    if (snapshot.checksum !== expectedChecksum) {
      damages.push({ code: "WORKSPACE_CHECKSUM_MISMATCH", message: "Workspace checksum does not match expected snapshot" });
    }
  }

  return {
    ok: damages.length === 0,
    damages
  };
}

export function migrateManifest(input: unknown): CompatibilityProjectManifest {
  if (typeof input !== "object" || input === null || Array.isArray(input)) {
    throw new Error("Manifest must be an object");
  }
  const record = input as Record<string, unknown>;
  if (record.schemaVersion === 1) {
    return normalizeCompatibilityManifest(record);
  }
  if (record.schemaVersion === 0 || record.version === 0) {
    const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : new Date(0).toISOString();
    return {
      ...createProjectManifest(
        typeof record.projectId === "string" ? record.projectId : "legacy_project",
        typeof record.name === "string" ? record.name : "Legacy Project",
        typeof record.createdAt === "string" ? record.createdAt : updatedAt
      ),
      updatedAt,
      entrypoints: {
        model: typeof record.modelPath === "string" ? record.modelPath : "model/olp-project.json"
      }
    };
  }
  throw new Error(`Unsupported manifest schema ${String(record.schemaVersion)}`);
}

export function stableStringify(value: unknown): string {
  return JSON.stringify(sortForStableStringify(value), null, 2);
}

function normalizeCompatibilityManifest(record: Record<string, unknown>): CompatibilityProjectManifest {
  if (record.schemaVersion !== 1) {
    throw new Error("Manifest schemaVersion must be 1");
  }
  if (typeof record.projectId !== "string" || typeof record.name !== "string") {
    throw new Error("Manifest projectId and name are required");
  }
  const createdAt = typeof record.createdAt === "string" ? record.createdAt : new Date(0).toISOString();
  const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : createdAt;
  const directories = isRecord(record.directories) ? record.directories : {};
  const entrypoints = isRecord(record.entrypoints) ? record.entrypoints : {};
  return {
    ...createProjectManifest(record.projectId, record.name, createdAt),
    updatedAt,
    directories: {
      model: stringField(directories.model, "model"),
      programs: stringField(directories.programs, "programs"),
      reports: stringField(directories.reports, "reports"),
      imports: stringField(directories.imports, "imports"),
      delivery: stringField(directories.delivery, "delivery"),
      traces: stringField(directories.traces, "traces")
    },
    entrypoints: {
      model: stringField(entrypoints.model, "model/olp-project.json")
    }
  };
}

function checksumFiles(files: Record<string, string>): string {
  const content = Object.entries(files)
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([path, value]) => `${path}\0${value}`)
    .join("\0");
  let hash = 0x811c9dc5;
  for (let index = 0; index < content.length; index += 1) {
    hash ^= content.charCodeAt(index);
    hash = Math.imul(hash, 0x01000193) >>> 0;
  }
  return `fnv1a32:${hash.toString(16).padStart(8, "0")}`;
}

function sortForStableStringify(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(sortForStableStringify);
  }
  if (isRecord(value)) {
    return Object.fromEntries(
      Object.entries(value)
        .filter(([, item]) => item !== undefined)
        .sort(([left], [right]) => left.localeCompare(right))
        .map(([key, item]) => [key, sortForStableStringify(item)])
    );
  }
  return value;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function stringField(value: unknown, fallback: string): string {
  return typeof value === "string" ? value : fallback;
}

export class ProjectWorkspace {
  constructor(private readonly store: WorkspaceStore) {}

  async createProject(model: OlpProjectModel): Promise<ProjectManifest> {
    const now = new Date().toISOString();
    const manifest = this.createManifest(model, now, now);
    await this.store.writeText(manifest.layout.projectJson, JSON.stringify(model, null, 2));
    await this.store.writeText(this.manifestPath(model.project.id), JSON.stringify(await this.refreshManifest(manifest), null, 2));
    return this.readManifest(model.project.id);
  }

  async readProject(projectId: string): Promise<OlpProjectModel> {
    const manifest = await this.readManifest(projectId);
    return JSON.parse(await this.store.readText(manifest.layout.projectJson)) as OlpProjectModel;
  }

  async writeJson(projectId: string, path: string, value: unknown): Promise<void> {
    await this.store.writeText(this.projectPath(projectId, path), JSON.stringify(value, null, 2));
    await this.rewriteManifest(projectId);
  }

  async readJson<T>(projectId: string, path: string): Promise<T> {
    return JSON.parse(await this.store.readText(this.projectPath(projectId, path))) as T;
  }

  async writeText(projectId: string, path: string, value: string): Promise<void> {
    await this.store.writeText(this.projectPath(projectId, path), value);
    await this.rewriteManifest(projectId);
  }

  async readText(projectId: string, path: string): Promise<string> {
    return this.store.readText(this.projectPath(projectId, path));
  }

  async delete(projectId: string, path: string): Promise<void> {
    await this.store.delete(this.projectPath(projectId, path));
    await this.rewriteManifest(projectId);
  }

  async snapshot(projectId: string, id = `snapshot-${Date.now()}`): Promise<WorkspaceSnapshot> {
    const manifest = await this.readManifest(projectId);
    const files = await this.projectFiles(projectId);
    const snapshot: WorkspaceSnapshot = { id, manifest, files };
    await this.store.writeText(`${manifest.layout.snapshotsDir}/${id}.json`, JSON.stringify(snapshot, null, 2));
    await this.rewriteManifest(projectId);
    return snapshot;
  }

  async restoreSnapshot(snapshot: WorkspaceSnapshot): Promise<ProjectManifest> {
    for (const [path, value] of Object.entries(snapshot.files)) {
      await this.store.writeText(path, value);
    }
    await this.store.writeText(this.manifestPath(snapshot.manifest.projectId), JSON.stringify(snapshot.manifest, null, 2));
    return this.rewriteManifest(snapshot.manifest.projectId);
  }

  async exportBundle(projectId: string): Promise<string> {
    const bundle: WorkspaceBundle = {
      format: "kdl-workspace-bundle/0.1",
      manifest: await this.readManifest(projectId),
      files: await this.projectFiles(projectId)
    };
    return JSON.stringify(bundle, null, 2);
  }

  async importBundle(text: string): Promise<ProjectManifest> {
    const bundle = JSON.parse(text) as WorkspaceBundle;
    if (bundle.format !== "kdl-workspace-bundle/0.1") {
      throw new Error(`Unsupported workspace bundle ${String(bundle.format)}`);
    }
    for (const [path, value] of Object.entries(bundle.files)) {
      await this.store.writeText(path, value);
    }
    await this.store.writeText(this.manifestPath(bundle.manifest.projectId), JSON.stringify(bundle.manifest, null, 2));
    return this.rewriteManifest(bundle.manifest.projectId);
  }

  async migrate(projectId: string, migrationId: string, apply: (model: OlpProjectModel) => OlpProjectModel): Promise<ProjectManifest> {
    const manifest = await this.readManifest(projectId);
    if (manifest.migrations.includes(migrationId)) {
      return manifest;
    }
    const model = apply(await this.readProject(projectId));
    model.metadata = {
      ...(model.metadata ?? {}),
      lastMigration: migrationId
    };
    await this.store.writeText(manifest.layout.projectJson, JSON.stringify(model, null, 2));
    const migrated = {
      ...manifest,
      migrations: [...manifest.migrations, migrationId],
      updatedAt: new Date().toISOString()
    };
    await this.store.writeText(this.manifestPath(projectId), JSON.stringify(await this.refreshManifest(migrated), null, 2));
    return this.readManifest(projectId);
  }

  async checkDamage(projectId: string): Promise<WorkspaceDamageReport> {
    const manifest = await this.readManifest(projectId);
    const missing: string[] = [];
    const checksumMismatch: string[] = [];
    for (const file of manifest.files) {
      try {
        const text = await this.store.readText(file.path);
        if (sha256(text) !== file.sha256) {
          checksumMismatch.push(file.path);
        }
      } catch {
        missing.push(file.path);
      }
    }
    const diagnostics = [
      ...missing.map((path) => `missing:${path}`),
      ...checksumMismatch.map((path) => `checksum:${path}`)
    ];
    return {
      ok: missing.length === 0 && checksumMismatch.length === 0,
      missing,
      checksumMismatch,
      diagnostics
    };
  }

  async readManifest(projectId: string): Promise<ProjectManifest> {
    return JSON.parse(await this.store.readText(this.manifestPath(projectId))) as ProjectManifest;
  }

  private createManifest(model: OlpProjectModel, createdAt: string, updatedAt: string): ProjectManifest {
    const root = `/projects/${model.project.id}`;
    return {
      schemaVersion: "workspace/0.1",
      projectId: model.project.id,
      projectName: model.project.name,
      ...(model.project.revision ? { revision: model.project.revision } : {}),
      createdAt,
      updatedAt,
      layout: {
        root,
        projectJson: `${root}/project.json`,
        programsDir: `${root}/programs`,
        reportsDir: `${root}/reports`,
        snapshotsDir: `${root}/snapshots`
      },
      files: [],
      migrations: []
    };
  }

  private async rewriteManifest(projectId: string): Promise<ProjectManifest> {
    const manifest = await this.readManifest(projectId);
    const refreshed = await this.refreshManifest({
      ...manifest,
      updatedAt: new Date().toISOString()
    });
    await this.store.writeText(this.manifestPath(projectId), JSON.stringify(refreshed, null, 2));
    return refreshed;
  }

  private async refreshManifest(manifest: ProjectManifest): Promise<ProjectManifest> {
    const files = await this.projectFiles(manifest.projectId);
    return {
      ...manifest,
      files: Object.entries(files)
        .map(([path, text]): WorkspaceFileEntry => ({
          path,
          kind: path.endsWith(".json") ? "json" : "text",
          sha256: sha256(text),
          bytes: Buffer.byteLength(text)
        }))
        .sort((left, right) => left.path.localeCompare(right.path))
    };
  }

  private async projectFiles(projectId: string): Promise<Record<string, string>> {
    const root = `/projects/${projectId}`;
    const paths = (await this.store.list(root)).filter((path) => path !== this.manifestPath(projectId));
    const entries = await Promise.all(paths.map(async (path) => [path, await this.store.readText(path)] as const));
    return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right)));
  }

  private manifestPath(projectId: string): string {
    return `/projects/${projectId}/project.manifest.json`;
  }

  private projectPath(projectId: string, path: string): string {
    const normalized = normalizePath(path);
    return normalized.startsWith(`/projects/${projectId}/`) ? normalized : `/projects/${projectId}/${normalized}`;
  }
}

function normalizePath(path: string): string {
  return path.replace(/\\/g, "/").replace(/\/+/g, "/").replace(/\/$/, "").replace(/^\.\//, "");
}

function sha256(text: string): string {
  return createHash("sha256").update(text).digest("hex");
}
