import { registerCommand } from "@open-mercato/shared/lib/commands";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { buildChanges, emitCrudSideEffects, emitCrudUndoSideEffects } from "@open-mercato/shared/lib/commands/helpers";
import { makeCreateRedo } from "@open-mercato/shared/lib/commands/redo";
import { StaffTimeEntry, StaffTimeEntrySegment, StaffTimeProject } from "../data/entities.js";
import { emitStaffEvent } from "../events.js";
const timeEntryCrudIndexer = {
  entityType: "staff:staff_time_entry"
};
import {
  staffTimeEntryCreateSchema,
  staffTimeEntryStartTimerSchema,
  staffTimeEntryUpdateSchema
} from "../data/validators.js";
import { staffTimeEntryCrudEvents } from "../lib/crud.js";
import {
  applyScopeToWhere,
  commandActorScope,
  commandInputScope,
  ensureOrganizationScope,
  ensureTenantScope,
  extractUndoPayload,
  scopeForDecryption,
  scopedStaffSnapshotWhere,
  staffSnapshotDecryptionScope,
  staffSnapshotScopeFromContext,
  staffSnapshotScopeFromSnapshot
} from "./shared.js";
import { getStaffMemberByUserId } from "../lib/staffMemberResolver.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("staff");
async function callerHasManageAll(ctx) {
  const userId = ctx.auth?.sub;
  if (!userId) return false;
  try {
    const rbac = ctx.container.resolve("rbacService");
    if (!rbac?.userHasAllFeatures) return false;
    return await rbac.userHasAllFeatures(
      userId,
      ["staff.timesheets.manage_all"],
      { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null }
    );
  } catch {
    return false;
  }
}
async function resolveCallerStaffMemberId(em, ctx) {
  const userId = ctx.auth?.sub;
  if (!userId) return null;
  const member = await getStaffMemberByUserId(
    em,
    userId,
    ctx.auth?.tenantId ?? null,
    ctx.auth?.orgId ?? null
  );
  return member?.id ?? null;
}
async function assertTimeProjectInScope(em, projectId, tenantId, organizationId) {
  if (!projectId) return;
  const exists = await em.findOne(
    StaffTimeProject,
    { id: projectId, tenantId, organizationId, deletedAt: null },
    { fields: ["id"] }
  );
  if (!exists) {
    const { translate } = await resolveTranslations();
    throw new CrudHttpError(422, {
      error: translate("staff.timesheets.errors.projectNotFound", "Time project not found or not accessible."),
      fieldErrors: {
        timeProjectId: translate("staff.timesheets.errors.projectNotFound", "Time project not found or not accessible.")
      }
    });
  }
}
async function loadTimeEntrySnapshot(em, id, scope) {
  const entry = await findOneWithDecryption(
    em,
    StaffTimeEntry,
    scopedStaffSnapshotWhere(id, scope),
    void 0,
    staffSnapshotDecryptionScope(scope)
  );
  if (!entry) return null;
  return {
    id: entry.id,
    tenantId: entry.tenantId,
    organizationId: entry.organizationId,
    staffMemberId: entry.staffMemberId,
    date: entry.date instanceof Date ? entry.date.toISOString().split("T")[0] : String(entry.date),
    durationMinutes: entry.durationMinutes,
    startedAt: entry.startedAt ? entry.startedAt.toISOString() : null,
    endedAt: entry.endedAt ? entry.endedAt.toISOString() : null,
    notes: entry.notes ?? null,
    timeProjectId: entry.timeProjectId ?? null,
    customerId: entry.customerId ?? null,
    dealId: entry.dealId ?? null,
    orderId: entry.orderId ?? null,
    source: entry.source,
    deletedAt: entry.deletedAt ? entry.deletedAt.toISOString() : null
  };
}
function timeEntrySeedFromSnapshot(snapshot) {
  return {
    id: snapshot.id,
    tenantId: snapshot.tenantId,
    organizationId: snapshot.organizationId,
    staffMemberId: snapshot.staffMemberId,
    date: snapshot.date,
    durationMinutes: snapshot.durationMinutes,
    startedAt: snapshot.startedAt ? new Date(snapshot.startedAt) : null,
    endedAt: snapshot.endedAt ? new Date(snapshot.endedAt) : null,
    notes: snapshot.notes ?? null,
    timeProjectId: snapshot.timeProjectId ?? null,
    customerId: snapshot.customerId ?? null,
    dealId: snapshot.dealId ?? null,
    orderId: snapshot.orderId ?? null,
    source: snapshot.source ?? "manual",
    createdAt: /* @__PURE__ */ new Date(),
    updatedAt: /* @__PURE__ */ new Date(),
    deletedAt: null
  };
}
const createTimeEntryCommand = {
  id: "staff.timesheets.time_entries.create",
  async execute(rawInput, ctx) {
    const parsed = staffTimeEntryCreateSchema.parse(rawInput);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    commandInputScope(ctx, parsed.tenantId, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    let effectiveStaffMemberId = parsed.staffMemberId;
    if (!await callerHasManageAll(ctx)) {
      const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx);
      if (!callerStaffMemberId) {
        const { translate } = await resolveTranslations();
        throw new CrudHttpError(403, {
          error: translate("staff.timesheets.errors.noStaffMember", "No staff member linked to your account.")
        });
      }
      effectiveStaffMemberId = callerStaffMemberId;
    }
    await assertTimeProjectInScope(em, parsed.timeProjectId ?? null, parsed.tenantId, parsed.organizationId);
    const now = /* @__PURE__ */ new Date();
    const entry = em.create(StaffTimeEntry, {
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      staffMemberId: effectiveStaffMemberId,
      date: parsed.date,
      durationMinutes: parsed.durationMinutes,
      startedAt: parsed.startedAt ?? null,
      endedAt: parsed.endedAt ?? null,
      notes: parsed.notes ?? null,
      timeProjectId: parsed.timeProjectId ?? null,
      customerId: parsed.customerId ?? null,
      dealId: parsed.dealId ?? null,
      orderId: parsed.orderId ?? null,
      source: parsed.source ?? "manual",
      createdAt: now,
      updatedAt: now,
      deletedAt: null
    });
    em.persist(entry);
    await em.flush();
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "created",
      entity: entry,
      identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
      events: staffTimeEntryCrudEvents,
      indexer: timeEntryCrudIndexer
    });
    return { timeEntryId: entry.id };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx));
    if (!snapshot) return null;
    return { snapshot };
  },
  buildLog: async ({ result, ctx }) => {
    const em = ctx.container.resolve("em").fork();
    const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx));
    if (!snapshot) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("staff.audit.timesheets.time_entries.create", "Create time entry"),
      resourceKind: "staff.timesheets.time_entry",
      resourceId: snapshot.id,
      tenantId: snapshot.tenantId,
      organizationId: snapshot.organizationId,
      snapshotAfter: snapshot,
      payload: {
        undo: {
          after: snapshot
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const after = payload?.after;
    if (!after) return;
    const em = ctx.container.resolve("em").fork();
    const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)));
    if (entry) {
      entry.deletedAt = /* @__PURE__ */ new Date();
      await em.flush();
      await emitCrudUndoSideEffects({
        dataEngine: ctx.container.resolve("dataEngine"),
        action: "deleted",
        entity: entry,
        identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
        events: staffTimeEntryCrudEvents
      });
    }
  },
  redo: makeCreateRedo({
    entityClass: StaffTimeEntry,
    getSnapshotId: (snapshot) => snapshot.id,
    seedFromSnapshot: timeEntrySeedFromSnapshot,
    buildResult: (entity) => ({ timeEntryId: entity.id }),
    events: staffTimeEntryCrudEvents,
    indexer: timeEntryCrudIndexer
  })
};
const startTimerCommand = {
  id: "staff.timesheets.time_entries.start_timer",
  async execute(rawInput, ctx) {
    const parsed = staffTimeEntryStartTimerSchema.parse(rawInput);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    commandInputScope(ctx, parsed.tenantId, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    let effectiveStaffMemberId = parsed.staffMemberId;
    if (!await callerHasManageAll(ctx)) {
      const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx);
      if (!callerStaffMemberId) {
        const { translate } = await resolveTranslations();
        throw new CrudHttpError(403, {
          error: translate("staff.timesheets.errors.noStaffMember", "No staff member linked to your account.")
        });
      }
      effectiveStaffMemberId = callerStaffMemberId;
    }
    await assertTimeProjectInScope(em, parsed.timeProjectId ?? null, parsed.tenantId, parsed.organizationId);
    const scopeCtx = { tenantId: parsed.tenantId, organizationId: parsed.organizationId };
    const { entry, startedAt } = await em.transactional(async (trx) => {
      const otherRunningEntry = await findOneWithDecryption(
        trx,
        StaffTimeEntry,
        {
          tenantId: parsed.tenantId,
          organizationId: parsed.organizationId,
          staffMemberId: effectiveStaffMemberId,
          startedAt: { $ne: null },
          endedAt: null,
          deletedAt: null
        },
        {},
        scopeCtx
      );
      if (otherRunningEntry) {
        const { translate } = await resolveTranslations();
        throw new CrudHttpError(409, {
          error: translate(
            "staff.timesheets.errors.timerAlreadyRunning",
            "Another timer is already running. Stop it before starting a new one."
          )
        });
      }
      const startedAt2 = /* @__PURE__ */ new Date();
      const entry2 = trx.create(StaffTimeEntry, {
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId,
        staffMemberId: effectiveStaffMemberId,
        date: parsed.date,
        durationMinutes: 0,
        startedAt: startedAt2,
        endedAt: null,
        notes: parsed.notes ?? null,
        timeProjectId: parsed.timeProjectId ?? null,
        customerId: null,
        dealId: null,
        orderId: null,
        source: "timer",
        createdAt: startedAt2,
        updatedAt: startedAt2,
        deletedAt: null
      });
      await trx.flush();
      const segmentData = {
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId,
        timeEntryId: entry2.id,
        startedAt: startedAt2,
        segmentType: "work"
      };
      trx.create(StaffTimeEntrySegment, segmentData);
      await trx.flush();
      return { entry: entry2, startedAt: startedAt2 };
    });
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "created",
      entity: entry,
      identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
      events: staffTimeEntryCrudEvents,
      indexer: timeEntryCrudIndexer
    });
    void emitStaffEvent("staff.timesheets.time_entry.timer_started", {
      id: entry.id,
      staffMemberId: effectiveStaffMemberId,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      startedAt: startedAt.toISOString()
    }, { persistent: true }).catch((err) => {
      logger.error("staff.timesheets emit timer_started failed", { err });
    });
    return { timeEntryId: entry.id };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx));
    if (!snapshot) return null;
    return { snapshot };
  },
  buildLog: async ({ result, ctx }) => {
    const em = ctx.container.resolve("em").fork();
    const snapshot = await loadTimeEntrySnapshot(em, result.timeEntryId, staffSnapshotScopeFromContext(ctx));
    if (!snapshot) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("staff.audit.timesheets.time_entries.startTimer", "Start timer"),
      resourceKind: "staff.timesheets.time_entry",
      resourceId: snapshot.id,
      tenantId: snapshot.tenantId,
      organizationId: snapshot.organizationId,
      snapshotAfter: snapshot,
      payload: {
        undo: {
          after: snapshot
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const after = payload?.after;
    if (!after) return;
    const em = ctx.container.resolve("em").fork();
    const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(after.id, staffSnapshotScopeFromSnapshot(after)));
    if (entry) {
      entry.deletedAt = /* @__PURE__ */ new Date();
      await em.flush();
      await emitCrudUndoSideEffects({
        dataEngine: ctx.container.resolve("dataEngine"),
        action: "deleted",
        entity: entry,
        identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
        events: staffTimeEntryCrudEvents
      });
    }
  }
};
const updateTimeEntryCommand = {
  id: "staff.timesheets.time_entries.update",
  async prepare(rawInput, ctx) {
    const parsed = staffTimeEntryUpdateSchema.parse(rawInput);
    const em = ctx.container.resolve("em");
    const snapshot = await loadTimeEntrySnapshot(em, parsed.id, staffSnapshotScopeFromContext(ctx));
    if (!snapshot) return {};
    return { before: snapshot };
  },
  async execute(rawInput, ctx) {
    const parsed = staffTimeEntryUpdateSchema.parse(rawInput);
    const em = ctx.container.resolve("em").fork();
    const scope = commandActorScope(ctx);
    const entry = await findOneWithDecryption(
      em,
      StaffTimeEntry,
      applyScopeToWhere({ id: parsed.id, deletedAt: null }, scope),
      void 0,
      scopeForDecryption(scope)
    );
    if (!entry) throw new CrudHttpError(404, { error: "Time entry not found." });
    ensureTenantScope(ctx, entry.tenantId);
    ensureOrganizationScope(ctx, entry.organizationId);
    if (!await callerHasManageAll(ctx)) {
      const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx);
      if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {
        const { translate } = await resolveTranslations();
        throw new CrudHttpError(403, {
          error: translate("staff.timesheets.errors.notOwner", "You can only manage your own time entries.")
        });
      }
    }
    if (parsed.timeProjectId !== void 0 && parsed.timeProjectId !== null) {
      await assertTimeProjectInScope(em, parsed.timeProjectId, entry.tenantId, entry.organizationId);
    }
    if (parsed.date !== void 0) entry.date = parsed.date;
    if (parsed.durationMinutes !== void 0) entry.durationMinutes = parsed.durationMinutes;
    if (parsed.timeProjectId !== void 0) entry.timeProjectId = parsed.timeProjectId ?? null;
    if (parsed.customerId !== void 0) entry.customerId = parsed.customerId ?? null;
    if (parsed.dealId !== void 0) entry.dealId = parsed.dealId ?? null;
    if (parsed.orderId !== void 0) entry.orderId = parsed.orderId ?? null;
    if (parsed.notes !== void 0) entry.notes = parsed.notes ?? null;
    entry.updatedAt = /* @__PURE__ */ new Date();
    await em.flush();
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "updated",
      entity: entry,
      identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
      events: staffTimeEntryCrudEvents,
      indexer: timeEntryCrudIndexer
    });
    return { timeEntryId: entry.id };
  },
  buildLog: async ({ snapshots, ctx }) => {
    const before = snapshots.before;
    if (!before) return null;
    const em = ctx.container.resolve("em").fork();
    const after = await loadTimeEntrySnapshot(em, before.id, staffSnapshotScopeFromSnapshot(before));
    if (!after) return null;
    const changes = buildChanges(before, after, [
      "date",
      "durationMinutes",
      "timeProjectId",
      "customerId",
      "dealId",
      "orderId",
      "notes",
      "deletedAt"
    ]);
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("staff.audit.timesheets.time_entries.update", "Update time entry"),
      resourceKind: "staff.timesheets.time_entry",
      resourceId: before.id,
      tenantId: before.tenantId,
      organizationId: before.organizationId,
      snapshotBefore: before,
      snapshotAfter: after,
      changes,
      payload: {
        undo: {
          before,
          after
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before;
    if (!before) return;
    const em = ctx.container.resolve("em").fork();
    const entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)));
    if (!entry) return;
    entry.date = before.date;
    entry.durationMinutes = before.durationMinutes;
    entry.timeProjectId = before.timeProjectId ?? null;
    entry.customerId = before.customerId ?? null;
    entry.dealId = before.dealId ?? null;
    entry.orderId = before.orderId ?? null;
    entry.notes = before.notes ?? null;
    entry.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null;
    entry.updatedAt = /* @__PURE__ */ new Date();
    await em.flush();
    await emitCrudUndoSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "updated",
      entity: entry,
      identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
      events: staffTimeEntryCrudEvents,
      indexer: timeEntryCrudIndexer
    });
  }
};
const deleteTimeEntryCommand = {
  id: "staff.timesheets.time_entries.delete",
  async prepare(input, ctx) {
    const id = input?.id;
    if (!id) throw new CrudHttpError(400, { error: "Time entry id is required." });
    const em = ctx.container.resolve("em");
    const snapshot = await loadTimeEntrySnapshot(em, id, staffSnapshotScopeFromContext(ctx));
    if (!snapshot) return {};
    return { before: snapshot };
  },
  async execute(input, ctx) {
    const id = input?.id;
    if (!id) throw new CrudHttpError(400, { error: "Time entry id is required." });
    const em = ctx.container.resolve("em").fork();
    const scope = commandActorScope(ctx);
    const entry = await findOneWithDecryption(
      em,
      StaffTimeEntry,
      applyScopeToWhere({ id, deletedAt: null }, scope),
      void 0,
      scopeForDecryption(scope)
    );
    if (!entry) throw new CrudHttpError(404, { error: "Time entry not found." });
    ensureTenantScope(ctx, entry.tenantId);
    ensureOrganizationScope(ctx, entry.organizationId);
    if (!await callerHasManageAll(ctx)) {
      const callerStaffMemberId = await resolveCallerStaffMemberId(em, ctx);
      if (!callerStaffMemberId || entry.staffMemberId !== callerStaffMemberId) {
        const { translate } = await resolveTranslations();
        throw new CrudHttpError(403, {
          error: translate("staff.timesheets.errors.notOwner", "You can only manage your own time entries.")
        });
      }
    }
    entry.deletedAt = /* @__PURE__ */ new Date();
    entry.updatedAt = /* @__PURE__ */ new Date();
    await em.flush();
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "deleted",
      entity: entry,
      identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
      events: staffTimeEntryCrudEvents,
      indexer: timeEntryCrudIndexer
    });
    return { timeEntryId: entry.id };
  },
  buildLog: async ({ snapshots }) => {
    const before = snapshots.before;
    if (!before) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("staff.audit.timesheets.time_entries.delete", "Delete time entry"),
      resourceKind: "staff.timesheets.time_entry",
      resourceId: before.id,
      tenantId: before.tenantId,
      organizationId: before.organizationId,
      snapshotBefore: before,
      payload: {
        undo: {
          before
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before;
    if (!before) return;
    const em = ctx.container.resolve("em").fork();
    let entry = await em.findOne(StaffTimeEntry, scopedStaffSnapshotWhere(before.id, staffSnapshotScopeFromSnapshot(before)));
    if (!entry) {
      entry = em.create(StaffTimeEntry, {
        id: before.id,
        tenantId: before.tenantId,
        organizationId: before.organizationId,
        staffMemberId: before.staffMemberId,
        date: before.date,
        durationMinutes: before.durationMinutes,
        startedAt: before.startedAt ? new Date(before.startedAt) : null,
        endedAt: before.endedAt ? new Date(before.endedAt) : null,
        notes: before.notes ?? null,
        timeProjectId: before.timeProjectId ?? null,
        customerId: before.customerId ?? null,
        dealId: before.dealId ?? null,
        orderId: before.orderId ?? null,
        source: before.source ?? "manual",
        deletedAt: null,
        createdAt: /* @__PURE__ */ new Date(),
        updatedAt: /* @__PURE__ */ new Date()
      });
      em.persist(entry);
    } else {
      entry.staffMemberId = before.staffMemberId;
      entry.date = before.date;
      entry.durationMinutes = before.durationMinutes;
      entry.startedAt = before.startedAt ? new Date(before.startedAt) : null;
      entry.endedAt = before.endedAt ? new Date(before.endedAt) : null;
      entry.notes = before.notes ?? null;
      entry.timeProjectId = before.timeProjectId ?? null;
      entry.customerId = before.customerId ?? null;
      entry.dealId = before.dealId ?? null;
      entry.orderId = before.orderId ?? null;
      entry.source = before.source ?? "manual";
      entry.deletedAt = null;
      entry.updatedAt = /* @__PURE__ */ new Date();
    }
    await em.flush();
    await emitCrudUndoSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "created",
      entity: entry,
      identifiers: { id: entry.id, organizationId: entry.organizationId, tenantId: entry.tenantId },
      events: staffTimeEntryCrudEvents,
      indexer: timeEntryCrudIndexer
    });
  }
};
registerCommand(createTimeEntryCommand);
registerCommand(startTimerCommand);
registerCommand(updateTimeEntryCommand);
registerCommand(deleteTimeEntryCommand);
//# sourceMappingURL=timesheets-entries.js.map
