import { join } from "node:path";

import { config } from "../config.js";
import {
  atomicWriteJson,
  ensureDirectory,
  listJsonFiles,
  readJsonFile,
  removeFileIfExists,
  slugify,
  toStableId,
} from "../utils/fileStore.js";

export type SessionStatus = "active" | "archived";

export type SessionRecord = {
  sessionId: string;
  actorKey: string;
  ownerUserId?: string;
  projectId?: string;
  projectKey: string;
  parentSessionId?: string;
  createdAt: string;
  updatedAt: string;
  status: SessionStatus;
  title?: string;
};

type SessionMetadataContext = {
  actorKey: string;
  userId?: string;
  projectId?: string;
  projectKey: string;
};

type EnsureSessionMetadataInput = SessionMetadataContext & {
  sessionId: string;
  parentSessionId?: string;
};

export class SessionMetadataOwnershipError extends Error {
  constructor(sessionId: string) {
    super(`session metadata ownership mismatch: ${sessionId}`);
    this.name = "SessionMetadataOwnershipError";
  }
}

export class SessionMetadataStore {
  constructor(private readonly baseDir = config.SESSION_METADATA_STORAGE_DIR) {}

  async initialize() {
    await ensureDirectory(this.baseDir);
  }

  async ensure(input: EnsureSessionMetadataInput) {
    const sessionId = normalizeSessionId(input.sessionId);
    if (!sessionId) {
      throw new Error("sessionId is required");
    }
    const existing = await this.readRecord(sessionId);
    if (existing) {
      if (!matchesContext(existing, input)) {
        throw new SessionMetadataOwnershipError(sessionId);
      }
      await atomicWriteJson(this.filePath(sessionId), existing);
      await removeFileIfExists(this.legacyFilePath(sessionId));
      return { created: false, record: existing };
    }

    const now = new Date().toISOString();
    const record: SessionRecord = {
      sessionId,
      actorKey: input.actorKey,
      ownerUserId: input.userId?.trim(),
      projectId: input.projectId,
      projectKey: input.projectKey,
      parentSessionId: normalizeSessionId(input.parentSessionId),
      createdAt: now,
      updatedAt: now,
      status: "active",
    };
    await atomicWriteJson(
      this.filePath(record.sessionId),
      record,
    );
    return { created: true, record };
  }

  async get(context: SessionMetadataContext, sessionId: string) {
    const normalizedSessionId = normalizeSessionId(sessionId);
    if (!normalizedSessionId) {
      return null;
    }
    const record = await this.readRecord(normalizedSessionId);
    return record && matchesContext(record, context) ? record : null;
  }

  async touch(
    record: SessionRecord,
    updates: Partial<Pick<SessionRecord, "title" | "status">> = {},
  ) {
    const next: SessionRecord = {
      ...record,
      ...normalizeSessionUpdates(updates),
      updatedAt: new Date().toISOString(),
    };
    await atomicWriteJson(
      this.filePath(record.sessionId),
      next,
    );
    await removeFileIfExists(this.legacyFilePath(record.sessionId));
    return next;
  }

  async list(context: SessionMetadataContext) {
    const files = await listJsonFiles(this.baseDir);
    const records = await Promise.all(
      files.map((file) => readJsonFile<SessionRecord>(file)),
    );
    const uniqueRecords = new Map<string, SessionRecord>();
    for (const record of records) {
      if (record && matchesContext(record, context)) {
        const previous = uniqueRecords.get(record.sessionId);
        if (!previous || record.updatedAt > previous.updatedAt) {
          uniqueRecords.set(record.sessionId, record);
        }
      }
    }
    return [...uniqueRecords.values()]
      .filter((record): record is SessionRecord => Boolean(record))
      .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
  }

  async remove(record: SessionRecord) {
    await Promise.all(
      this.filePaths(record.sessionId).map((path) => removeFileIfExists(path)),
    );
  }

  private filePath(sessionId: string) {
    return join(
      this.baseDir,
      `${slugify(sessionId)}-${toStableId(sessionId)}.json`,
    );
  }

  private legacyFilePath(sessionId: string) {
    return join(this.baseDir, `${slugify(sessionId)}.json`);
  }

  private filePaths(sessionId: string) {
    return [this.filePath(sessionId), this.legacyFilePath(sessionId)];
  }

  private async readRecord(sessionId: string) {
    for (const path of this.filePaths(sessionId)) {
      const record = await readJsonFile<SessionRecord>(path);
      if (record?.sessionId === sessionId) {
        return record;
      }
    }
    return null;
  }
}

const matchesContext = (
  record: SessionRecord,
  context: SessionMetadataContext,
) =>
  record.actorKey === context.actorKey &&
  record.projectKey === context.projectKey &&
  (!record.ownerUserId || record.ownerUserId === context.userId?.trim()) &&
  (!record.projectId || record.projectId === context.projectId);

const normalizeSessionId = (value?: string) => {
  const normalized = value?.trim();
  return normalized ? normalized.slice(0, 128) : undefined;
};

const normalizeSessionUpdates = (
  updates: Partial<Pick<SessionRecord, "title" | "status">>,
) => {
  const normalized: Partial<Pick<SessionRecord, "title" | "status">> = {};
  if (updates.status === "active" || updates.status === "archived") {
    normalized.status = updates.status;
  }
  if (typeof updates.title === "string") {
    const trimmed = updates.title.trim();
    if (trimmed) {
      normalized.title = trimmed.slice(0, 120);
    }
  }
  return normalized;
};
