import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import {
  buildOptimisticLockConflictBody,
  enforceCommandOptimisticLock,
  enforceRecordGoneIsConflict
} from "@open-mercato/shared/lib/crud/optimistic-lock-command";
import { Perspective, RolePerspective } from "../data/entities.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("perspectives").child({ component: "perspective-service" });
const PERSPECTIVE_RESOURCE_KIND = "perspectives.perspective";
const ROLE_PERSPECTIVE_RESOURCE_KIND = "perspectives.role_perspective";
function resolveRoleLockInput(expectedUpdatedAtByRoleId, roleId, request) {
  if (expectedUpdatedAtByRoleId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByRoleId, roleId)) {
    return { expected: expectedUpdatedAtByRoleId[roleId] ?? null, request: null };
  }
  return { expected: void 0, request: request ?? null };
}
function resolveRoleRecordLockInput(expectedUpdatedAtByPerspectiveId, expectedUpdatedAtByRoleId, record, request) {
  if (expectedUpdatedAtByPerspectiveId && Object.prototype.hasOwnProperty.call(expectedUpdatedAtByPerspectiveId, record.id)) {
    return { expected: expectedUpdatedAtByPerspectiveId[record.id] ?? null, request: null };
  }
  return resolveRoleLockInput(expectedUpdatedAtByRoleId, record.roleId, request);
}
function firstExpectedUpdatedAt(expectedUpdatedAtById) {
  if (!expectedUpdatedAtById) return null;
  for (const value of Object.values(expectedUpdatedAtById)) {
    if (value instanceof Date) return value;
    if (typeof value === "string" && value.trim().length > 0) return value;
  }
  return null;
}
function throwMissingRoleRecordVersionConflict(record) {
  const current = record.updatedAt instanceof Date && Number.isFinite(record.updatedAt.getTime()) ? record.updatedAt.toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
  throw new CrudHttpError(409, buildOptimisticLockConflictBody(current, current));
}
const CACHE_TTL_MS = 5 * 60 * 1e3;
const nullish = (value) => value == null ? null : value;
const scopeKey = (scope) => `${scope.userId}:${scope.tenantId ?? "null"}:${scope.organizationId ?? "null"}`;
const userCacheKey = (scope, tableId, roleIds) => `perspectives:user-state:${scopeKey(scope)}:${tableId}:${roleIds.sort((a, b) => a.localeCompare(b)).join(",")}`;
const userTag = (scope, tableId) => tableId ? `perspectives:user:${scopeKey(scope)}:${tableId}` : `perspectives:user:${scopeKey(scope)}`;
const roleTag = (roleId, tableId, tenantId) => {
  const tenant = tenantId ?? "null";
  return tableId ? `perspectives:role:${roleId}:${tenant}:${tableId}` : `perspectives:role:${roleId}:${tenant}`;
};
function isResolvedPerspective(value) {
  if (typeof value !== "object" || value === null) return false;
  const record = value;
  return typeof record.id === "string" && typeof record.name === "string" && typeof record.tableId === "string" && typeof record.isDefault === "boolean" && typeof record.createdAt === "string";
}
function isResolvedRolePerspective(value) {
  if (typeof value !== "object" || value === null) return false;
  const record = value;
  return typeof record.id === "string" && typeof record.roleId === "string" && typeof record.tableId === "string" && typeof record.name === "string" && typeof record.isDefault === "boolean" && typeof record.createdAt === "string";
}
function isPerspectivesState(value) {
  if (typeof value !== "object" || value === null) return false;
  const record = value;
  if (typeof record.tableId !== "string") return false;
  if (!Array.isArray(record.personal) || record.personal.some((item) => !isResolvedPerspective(item))) return false;
  if (record.personalDefaultId !== null && typeof record.personalDefaultId !== "string") return false;
  if (!Array.isArray(record.rolePerspectives) || record.rolePerspectives.some((item) => !isResolvedRolePerspective(item))) return false;
  return true;
}
function maybeMigrateLegacyFilterValues(settings) {
  const filters = settings.filters;
  if (!filters || typeof filters !== "object") return settings;
  const record = filters;
  if ("v" in record && record.v === 2) return settings;
  if ("root" in record) return settings;
  logger.warn("Dropping legacy filterValues shape; please re-create the perspective with the new filter UI.");
  return { ...settings, filters: void 0 };
}
function toResolvedPerspective(entity) {
  const settings = maybeMigrateLegacyFilterValues(entity.settingsJson ?? {});
  return {
    id: entity.id,
    name: entity.name,
    tableId: entity.tableId,
    isDefault: !!entity.isDefault,
    settings,
    createdAt: entity.createdAt.toISOString(),
    updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null
  };
}
function toResolvedRolePerspective(entity) {
  const settings = maybeMigrateLegacyFilterValues(entity.settingsJson ?? {});
  return {
    id: entity.id,
    roleId: entity.roleId,
    tableId: entity.tableId,
    name: entity.name,
    isDefault: !!entity.isDefault,
    settings,
    tenantId: nullish(entity.tenantId),
    organizationId: nullish(entity.organizationId),
    createdAt: entity.createdAt.toISOString(),
    updatedAt: entity.updatedAt ? entity.updatedAt.toISOString() : null
  };
}
async function loadPerspectivesState(em, cache, options) {
  const { scope, tableId } = options;
  const roleIds = Array.isArray(options.roleIds) ? options.roleIds.filter((id) => id && id.length > 0) : [];
  const uniqueRoles = Array.from(new Set(roleIds));
  const cacheKey = cache && uniqueRoles.length <= 16 ? userCacheKey(scope, tableId, uniqueRoles) : null;
  if (cache && cacheKey) {
    const cached = await cache.get(cacheKey);
    if (cached && isPerspectivesState(cached)) return cached;
  }
  const tenantId = scope.tenantId ?? null;
  const organizationId = scope.organizationId ?? null;
  const [personal, roleRecords] = await Promise.all([
    em.find(Perspective, {
      userId: scope.userId,
      tenantId,
      organizationId,
      tableId,
      deletedAt: null
    }, { orderBy: { updatedAt: "desc" } }),
    uniqueRoles.length ? em.find(RolePerspective, {
      roleId: { $in: uniqueRoles },
      tableId,
      deletedAt: null,
      $and: [
        { $or: [{ tenantId }, { tenantId: null }] },
        { $or: [{ organizationId }, { organizationId: null }] }
      ]
    }, { orderBy: { updatedAt: "desc" } }) : []
  ]);
  const personalResolved = personal.map(toResolvedPerspective);
  const personalDefaultId = personalResolved.find((p) => p.isDefault)?.id ?? null;
  const roleResolved = roleRecords.map(toResolvedRolePerspective);
  const state = {
    tableId,
    personal: personalResolved,
    personalDefaultId,
    rolePerspectives: roleResolved
  };
  if (cache && cacheKey) {
    await cache.set(cacheKey, state, {
      ttl: CACHE_TTL_MS,
      tags: [
        userTag(scope, tableId),
        ...uniqueRoles.map((roleId) => roleTag(roleId, tableId, tenantId))
      ]
    });
  }
  return state;
}
async function saveUserPerspective(em, cache, options) {
  const { scope, tableId, input } = options;
  const tenantId = scope.tenantId ?? null;
  const organizationId = scope.organizationId ?? null;
  let entity = null;
  if (input.perspectiveId) {
    entity = await em.findOne(Perspective, {
      id: input.perspectiveId,
      userId: scope.userId,
      tenantId,
      organizationId,
      tableId,
      deletedAt: null
    });
    if (!entity) {
      throw Object.assign(new Error("Perspective not found"), { code: "NOT_FOUND" });
    }
    enforceCommandOptimisticLock({
      resourceKind: PERSPECTIVE_RESOURCE_KIND,
      resourceId: entity.id,
      current: entity.updatedAt ?? null,
      request: options.request ?? null
    });
    if (entity.name !== input.name) {
      const duplicate = await em.findOne(Perspective, {
        userId: scope.userId,
        tenantId,
        organizationId,
        tableId,
        name: input.name,
        id: { $ne: entity.id },
        deletedAt: null
      });
      if (duplicate) {
        throw new CrudHttpError(409, {
          error: "A view with this name already exists.",
          code: "duplicate_name"
        });
      }
    }
  } else {
    entity = await em.findOne(Perspective, {
      userId: scope.userId,
      tenantId,
      organizationId,
      tableId,
      name: input.name,
      deletedAt: null
    });
  }
  const now = /* @__PURE__ */ new Date();
  if (!entity) {
    entity = em.create(Perspective, {
      userId: scope.userId,
      tenantId,
      organizationId,
      tableId,
      name: input.name,
      settingsJson: input.settings,
      isDefault: Boolean(input.isDefault),
      createdAt: now,
      updatedAt: now
    });
    em.persist(entity);
  } else {
    entity.name = input.name;
    entity.settingsJson = input.settings;
    entity.updatedAt = now;
    if (input.isDefault === true) entity.isDefault = true;
    if (input.isDefault === false) entity.isDefault = false;
  }
  if (input.isDefault === true) {
    await em.nativeUpdate(
      Perspective,
      {
        userId: scope.userId,
        tenantId,
        organizationId,
        tableId,
        id: { $ne: entity.id },
        deletedAt: null
      },
      { isDefault: false, updatedAt: now }
    );
    entity.isDefault = true;
  }
  await em.flush();
  if (cache?.deleteByTags) {
    await cache.deleteByTags([userTag(scope, tableId)]);
  }
  return toResolvedPerspective(entity);
}
async function deleteUserPerspective(em, cache, options) {
  const { scope, tableId, perspectiveId } = options;
  const tenantId = scope.tenantId ?? null;
  const organizationId = scope.organizationId ?? null;
  const existing = await em.findOne(Perspective, {
    id: perspectiveId,
    userId: scope.userId,
    tenantId,
    organizationId,
    tableId,
    deletedAt: null
  });
  if (!existing) {
    enforceRecordGoneIsConflict({
      resourceKind: PERSPECTIVE_RESOURCE_KIND,
      resourceId: perspectiveId,
      request: options.request ?? null
    });
    return false;
  }
  enforceCommandOptimisticLock({
    resourceKind: PERSPECTIVE_RESOURCE_KIND,
    resourceId: existing.id,
    current: existing.updatedAt ?? null,
    request: options.request ?? null
  });
  existing.deletedAt = /* @__PURE__ */ new Date();
  existing.isDefault = false;
  await em.flush();
  if (cache?.deleteByTags) {
    await cache.deleteByTags([userTag(scope, tableId)]);
  }
  return true;
}
async function saveRolePerspectives(em, cache, options) {
  const { tableId, input } = options;
  const tenantId = options.tenantId ?? null;
  const organizationId = options.organizationId ?? null;
  const now = /* @__PURE__ */ new Date();
  const touchedRoleIds = /* @__PURE__ */ new Set();
  const resultRecords = [];
  const recordByRole = /* @__PURE__ */ new Map();
  if (input.roleIds.length) {
    const existingRecords = await em.find(RolePerspective, {
      roleId: { $in: input.roleIds },
      tableId,
      tenantId,
      organizationId,
      name: input.name,
      deletedAt: null
    });
    for (const existing of existingRecords) recordByRole.set(existing.roleId, existing);
  }
  const defaultRecordsByRole = /* @__PURE__ */ new Map();
  if (input.setDefault === true && input.roleIds.length) {
    const existingDefaultRecords = await em.find(RolePerspective, {
      roleId: { $in: input.roleIds },
      tableId,
      tenantId,
      organizationId,
      isDefault: true,
      deletedAt: null
    });
    for (const existing of existingDefaultRecords) {
      const records = defaultRecordsByRole.get(existing.roleId) ?? [];
      records.push(existing);
      defaultRecordsByRole.set(existing.roleId, records);
    }
  }
  for (const roleId of input.roleIds) {
    let record = recordByRole.get(roleId) ?? null;
    if (!record) {
      record = em.create(RolePerspective, {
        roleId,
        tableId,
        tenantId,
        organizationId,
        name: input.name,
        settingsJson: input.settings,
        isDefault: Boolean(input.setDefault),
        createdAt: now,
        updatedAt: now
      });
      em.persist(record);
      recordByRole.set(roleId, record);
    } else {
      enforceCommandOptimisticLock({
        resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
        resourceId: record.id,
        current: record.updatedAt ?? null,
        ...resolveRoleRecordLockInput(
          options.expectedUpdatedAtByPerspectiveId,
          options.expectedUpdatedAtByRoleId,
          record,
          options.request ?? null
        )
      });
      record.settingsJson = input.settings;
      record.updatedAt = now;
      if (input.setDefault === true) record.isDefault = true;
      if (input.setDefault === false) record.isDefault = false;
    }
    if (input.setDefault === true) {
      for (const defaultRecord of defaultRecordsByRole.get(roleId) ?? []) {
        if (defaultRecord.id === record.id) continue;
        enforceCommandOptimisticLock({
          resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
          resourceId: defaultRecord.id,
          current: defaultRecord.updatedAt ?? null,
          ...resolveRoleRecordLockInput(
            options.expectedUpdatedAtByPerspectiveId,
            options.expectedUpdatedAtByRoleId,
            defaultRecord,
            options.request ?? null
          )
        });
      }
      await em.nativeUpdate(
        RolePerspective,
        {
          roleId,
          tableId,
          tenantId,
          organizationId,
          id: { $ne: record.id },
          isDefault: true,
          deletedAt: null
        },
        { isDefault: false, updatedAt: now }
      );
      record.isDefault = true;
    }
    touchedRoleIds.add(roleId);
    resultRecords.push(record);
  }
  if (input.roleIds.length) {
    await em.flush();
  }
  if (cache?.deleteByTags && touchedRoleIds.size > 0) {
    const tags = Array.from(touchedRoleIds).map((roleId) => roleTag(roleId, tableId, tenantId));
    await cache.deleteByTags(tags);
  }
  return resultRecords.map(toResolvedRolePerspective);
}
async function clearRolePerspectives(em, cache, options) {
  const { tableId, roleIds } = options;
  const tenantId = options.tenantId ?? null;
  const organizationId = options.organizationId ?? null;
  if (!roleIds.length) return 0;
  const existingRecords = await em.find(RolePerspective, {
    roleId: { $in: roleIds },
    tableId,
    tenantId,
    organizationId,
    deletedAt: null
  });
  const recordsByRole = /* @__PURE__ */ new Map();
  for (const record of existingRecords) {
    const records = recordsByRole.get(record.roleId) ?? [];
    records.push(record);
    recordsByRole.set(record.roleId, records);
  }
  for (const roleId of roleIds) {
    const records = recordsByRole.get(roleId) ?? [];
    const lockInput = resolveRoleLockInput(options.expectedUpdatedAtByRoleId, roleId, options.request ?? null);
    if (!records.length) {
      const expected = firstExpectedUpdatedAt(options.expectedUpdatedAtByPerspectiveId);
      enforceRecordGoneIsConflict({
        resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
        resourceId: roleId,
        expected: expected ?? lockInput.expected,
        request: expected ? null : lockInput.request
      });
      continue;
    }
    for (const record of records) {
      if (options.expectedUpdatedAtByPerspectiveId && !Object.prototype.hasOwnProperty.call(options.expectedUpdatedAtByPerspectiveId, record.id)) {
        throwMissingRoleRecordVersionConflict(record);
      }
      enforceCommandOptimisticLock({
        resourceKind: ROLE_PERSPECTIVE_RESOURCE_KIND,
        resourceId: record.id,
        current: record.updatedAt ?? null,
        ...resolveRoleRecordLockInput(
          options.expectedUpdatedAtByPerspectiveId,
          options.expectedUpdatedAtByRoleId,
          record,
          options.request ?? null
        )
      });
    }
  }
  const affected = await em.nativeUpdate(
    RolePerspective,
    {
      roleId: { $in: roleIds },
      tableId,
      tenantId,
      organizationId,
      deletedAt: null
    },
    { deletedAt: /* @__PURE__ */ new Date(), isDefault: false }
  );
  if (cache?.deleteByTags) {
    const tags = roleIds.map((roleId) => roleTag(roleId, tableId, tenantId));
    await cache.deleteByTags(tags);
  }
  return affected;
}
export {
  clearRolePerspectives,
  deleteUserPerspective,
  loadPerspectivesState,
  maybeMigrateLegacyFilterValues,
  saveRolePerspectives,
  saveUserPerspective
};
//# sourceMappingURL=perspectiveService.js.map
