import { registerCommand } from "@open-mercato/shared/lib/commands";
import { parseAvailabilityRuleWindow } from "@open-mercato/core/modules/planner/lib/availabilitySchedule";
import { PlannerAvailabilityRule, PlannerAvailabilityRuleSet } from "../data/entities.js";
import {
  plannerAvailabilityWeeklyReplaceSchema
} from "../data/validators.js";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
import {
  enforceCommandOptimisticLock,
  enforceRecordGoneIsConflict
} from "@open-mercato/shared/lib/crud/optimistic-lock-command";
import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from "./shared.js";
import { plannerAvailabilityRuleSetCrudEvents } from "../lib/crud.js";
import { E } from "../../../generated/entities.ids.generated.js";
const AVAILABILITY_RULE_RESOURCE_KIND = "planner.availability.rule";
const AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND = "planner.availability-rule-set";
const availabilityRuleSetCrudIndexer = {
  entityType: E.planner.planner_availability_rule_set
};
const AVAILABILITY_RULE_SET_RESOURCE_KIND = "planner.availability.rule.set";
const DAY_CODES = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
function parseTimeInput(value) {
  const [hours, minutes] = value.split(":").map((part) => Number(part));
  if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
  if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
  return { hours, minutes };
}
function toDateForWeekday(weekday, time) {
  const parsed = parseTimeInput(time);
  if (!parsed) return null;
  const now = /* @__PURE__ */ new Date();
  const base = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const diff = (weekday - base.getDay() + 7) % 7;
  const target = new Date(base.getTime() + diff * 24 * 60 * 60 * 1e3);
  target.setHours(parsed.hours, parsed.minutes, 0, 0);
  return target;
}
function formatDuration(minutes) {
  const clamped = Math.max(1, minutes);
  const hours = Math.floor(clamped / 60);
  const mins = clamped % 60;
  if (hours > 0 && mins > 0) return `PT${hours}H${mins}M`;
  if (hours > 0) return `PT${hours}H`;
  return `PT${mins}M`;
}
function buildWeeklyRrule(start, end) {
  const dtStart = start.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
  const durationMinutes = Math.max(1, Math.round((end.getTime() - start.getTime()) / 6e4));
  const duration = formatDuration(durationMinutes);
  const dayCode = DAY_CODES[start.getDay()] ?? "MO";
  return `DTSTART:${dtStart}
DURATION:${duration}
RRULE:FREQ=WEEKLY;BYDAY=${dayCode}`;
}
function toAvailabilityRuleSnapshot(record) {
  return {
    id: record.id,
    tenantId: record.tenantId,
    organizationId: record.organizationId,
    subjectType: record.subjectType,
    subjectId: record.subjectId,
    timezone: record.timezone,
    rrule: record.rrule,
    exdates: [...record.exdates ?? []],
    kind: record.kind,
    note: record.note ?? null,
    deletedAt: record.deletedAt ?? null
  };
}
function nextRuleSetUpdatedAt(current, fallback) {
  const currentMs = current instanceof Date ? current.getTime() : Number.NaN;
  if (Number.isFinite(currentMs) && fallback.getTime() <= currentMs) {
    return new Date(currentMs + 1);
  }
  return fallback;
}
async function loadWeeklySnapshots(em, params) {
  const existing = await em.find(PlannerAvailabilityRule, {
    tenantId: params.tenantId,
    organizationId: params.organizationId,
    subjectType: params.subjectType,
    subjectId: params.subjectId,
    deletedAt: null
  });
  return existing.filter((rule) => {
    const repeat = parseAvailabilityRuleWindow(rule).repeat;
    return repeat === "weekly" || repeat === "daily";
  }).map(toAvailabilityRuleSnapshot);
}
async function restoreAvailabilityRuleFromSnapshot(em, snapshot) {
  let record = await em.findOne(PlannerAvailabilityRule, { id: snapshot.id });
  if (!record) {
    record = em.create(PlannerAvailabilityRule, {
      id: snapshot.id,
      tenantId: snapshot.tenantId,
      organizationId: snapshot.organizationId,
      subjectType: snapshot.subjectType,
      subjectId: snapshot.subjectId,
      timezone: snapshot.timezone,
      rrule: snapshot.rrule,
      exdates: snapshot.exdates ?? [],
      kind: snapshot.kind ?? "availability",
      note: snapshot.note ?? null,
      createdAt: /* @__PURE__ */ new Date(),
      updatedAt: /* @__PURE__ */ new Date(),
      deletedAt: snapshot.deletedAt ?? null
    });
    em.persist(record);
  } else {
    record.subjectType = snapshot.subjectType;
    record.subjectId = snapshot.subjectId;
    record.timezone = snapshot.timezone;
    record.rrule = snapshot.rrule;
    record.exdates = snapshot.exdates ?? [];
    record.kind = snapshot.kind ?? "availability";
    record.note = snapshot.note ?? null;
    record.deletedAt = snapshot.deletedAt ?? null;
  }
}
const replaceWeeklyAvailabilityCommand = {
  id: "planner.availability.weekly.replace",
  async prepare(input, ctx) {
    const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    const em = ctx.container.resolve("em");
    const before = await loadWeeklySnapshots(em, {
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      subjectType: parsed.subjectType,
      subjectId: parsed.subjectId
    });
    return { before };
  },
  async execute(input, ctx) {
    const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    const now = /* @__PURE__ */ new Date();
    const touchedRuleSet = await em.transactional(async (trx) => {
      let ruleSet = null;
      if (parsed.subjectType === "ruleset") {
        ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {
          id: parsed.subjectId,
          tenantId: parsed.tenantId,
          organizationId: parsed.organizationId,
          deletedAt: null
        });
        if (ruleSet) {
          enforceCommandOptimisticLock({
            resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,
            resourceId: ruleSet.id,
            current: ruleSet.updatedAt,
            request: ctx.request ?? null
          });
        } else {
          enforceRecordGoneIsConflict({
            resourceKind: AVAILABILITY_RULE_SET_RESOURCE_KIND,
            resourceId: parsed.subjectId,
            request: ctx.request ?? null
          });
        }
      }
      const existing = await trx.find(PlannerAvailabilityRule, {
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId,
        subjectType: parsed.subjectType,
        subjectId: parsed.subjectId,
        deletedAt: null
      });
      const toDelete = existing.filter((rule) => {
        const repeat = parseAvailabilityRuleWindow(rule).repeat;
        return repeat === "weekly" || repeat === "daily";
      });
      toDelete.forEach((rule) => {
        rule.deletedAt = now;
        rule.updatedAt = now;
      });
      if (toDelete.length) {
        trx.persist(toDelete);
      }
      parsed.windows.forEach((window) => {
        const start = toDateForWeekday(window.weekday, window.start);
        const end = toDateForWeekday(window.weekday, window.end);
        if (!start || !end || start >= end) return;
        const rrule = buildWeeklyRrule(start, end);
        const record = trx.create(PlannerAvailabilityRule, {
          tenantId: parsed.tenantId,
          organizationId: parsed.organizationId,
          subjectType: parsed.subjectType,
          subjectId: parsed.subjectId,
          timezone: parsed.timezone,
          rrule,
          exdates: [],
          kind: "availability",
          note: null,
          createdAt: now,
          updatedAt: now
        });
        trx.persist(record);
      });
      if (ruleSet) {
        ruleSet.updatedAt = nextRuleSetUpdatedAt(ruleSet.updatedAt, now);
        trx.persist(ruleSet);
      }
      await trx.flush();
      return ruleSet;
    });
    if (touchedRuleSet) {
      const dataEngine = ctx.container.resolve("dataEngine");
      await emitCrudSideEffects({
        dataEngine,
        action: "updated",
        entity: touchedRuleSet,
        identifiers: {
          id: touchedRuleSet.id,
          organizationId: touchedRuleSet.organizationId,
          tenantId: touchedRuleSet.tenantId
        },
        events: plannerAvailabilityRuleSetCrudEvents,
        indexer: availabilityRuleSetCrudIndexer
      });
    }
    return { ok: true };
  },
  buildLog: async ({ input, snapshots, ctx }) => {
    const parsed = plannerAvailabilityWeeklyReplaceSchema.parse(input);
    const em = ctx.container.resolve("em").fork();
    const after = await loadWeeklySnapshots(em, {
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      subjectType: parsed.subjectType,
      subjectId: parsed.subjectId
    });
    const before = snapshots.before ?? [];
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("planner.audit.availability.weekly.replace", "Replace weekly availability"),
      resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,
      resourceId: null,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      snapshotBefore: before,
      snapshotAfter: after,
      payload: {
        undo: {
          before,
          after
        }
      },
      context: parsed.subjectType === "ruleset" ? { cacheAliases: [AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND] } : null
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before ?? [];
    const after = payload?.after ?? [];
    if (!before.length && !after.length) return;
    const em = ctx.container.resolve("em").fork();
    await em.transactional(async (trx) => {
      if (after.length) {
        const ids = after.map((rule) => rule.id);
        const records = await trx.find(PlannerAvailabilityRule, { id: { $in: ids } });
        records.forEach((record) => {
          record.deletedAt = /* @__PURE__ */ new Date();
        });
        if (records.length) trx.persist(records);
      }
      for (const snapshot of before) {
        await restoreAvailabilityRuleFromSnapshot(trx, { ...snapshot, deletedAt: null });
      }
      await trx.flush();
    });
  }
};
registerCommand(replaceWeeklyAvailabilityCommand);
//# sourceMappingURL=availability-weekly.js.map
