import { z } from "zod";
import { AttendeeStatus, EntityId } from "./entities";

/** Longest window a single calendar query may ask for. Mirrors
 * MAX_WINDOW_DAYS in libs/core/src/recurrence.ts — recurring series are
 * expanded per request, so an unbounded range is an unbounded response. */
export const MAX_EVENT_WINDOW_DAYS = 92;

/** The stored RRULE subset. Validated by shape here and parsed for real by
 * parseRecurrenceRule in libs/core; this keeps obvious junk out of the
 * column without duplicating the parser in a regex. */
export const RecurrenceRuleString = z
  .string()
  .max(200)
  .regex(/^FREQ=(DAILY|WEEKLY|MONTHLY)(;[A-Z]+=[^;]+)*$/, "unsupported recurrence rule");

const EventFields = {
  title: z.string().min(1).max(200),
  description: z.string().max(10_000).nullable().optional(),
  location: z.string().max(300).nullable().optional(),
  startsAt: z.string().datetime(),
  endsAt: z.string().datetime(),
  allDay: z.boolean().optional(),
  /** IANA zone the event was authored in; defaults to UTC server-side. */
  timezone: z.string().min(1).max(64).optional(),
  recurrenceRule: RecurrenceRuleString.nullable().optional(),
  channelId: EntityId.nullable().optional(),
  attendeeUserIds: z.array(EntityId).max(200).optional(),
};

export const CreateEventRequest = z.object(EventFields).refine((v) => new Date(v.endsAt) >= new Date(v.startsAt), {
  message: "endsAt must not precede startsAt",
  path: ["endsAt"],
});
export type CreateEventRequest = z.infer<typeof CreateEventRequest>;

/**
 * Which slice of a repeating event a write applies to. "series" rewrites the
 * stored row (every occurrence); "occurrence" detaches just the one named by
 * occurrenceDate. A one-off event only ever takes "series".
 */
export const EventWriteScope = z.enum(["series", "occurrence"]);
export type EventWriteScope = z.infer<typeof EventWriteScope>;

// Partial update — an explicit `null` clears a nullable field, an omitted key
// leaves it untouched, same idiom as UpdateTaskRequest.
export const UpdateEventRequest = z
  .object({
    scope: EventWriteScope.default("series"),
    /** Required when scope is "occurrence": which instance to detach. */
    occurrenceDate: z
      .string()
      .regex(/^\d{4}-\d{2}-\d{2}$/, "expected YYYY-MM-DD")
      .optional(),
    title: EventFields.title.optional(),
    description: EventFields.description,
    location: EventFields.location,
    startsAt: z.string().datetime().optional(),
    endsAt: z.string().datetime().optional(),
    allDay: z.boolean().optional(),
    timezone: EventFields.timezone,
    recurrenceRule: EventFields.recurrenceRule,
    channelId: EventFields.channelId,
  })
  .refine((v) => v.scope !== "occurrence" || Boolean(v.occurrenceDate), {
    message: "occurrenceDate is required when scope is 'occurrence'",
    path: ["occurrenceDate"],
  })
  .refine((v) => !v.startsAt || !v.endsAt || new Date(v.endsAt) >= new Date(v.startsAt), {
    message: "endsAt must not precede startsAt",
    path: ["endsAt"],
  })
  // Changing the recurrence rule of a single detached occurrence is
  // meaningless — the rule lives on the series.
  .refine((v) => v.scope !== "occurrence" || v.recurrenceRule === undefined, {
    message: "recurrenceRule can only be changed with scope 'series'",
    path: ["recurrenceRule"],
  });
export type UpdateEventRequest = z.infer<typeof UpdateEventRequest>;

export const DeleteEventRequest = z
  .object({
    scope: EventWriteScope.default("series"),
    occurrenceDate: z
      .string()
      .regex(/^\d{4}-\d{2}-\d{2}$/, "expected YYYY-MM-DD")
      .optional(),
  })
  .refine((v) => v.scope !== "occurrence" || Boolean(v.occurrenceDate), {
    message: "occurrenceDate is required when scope is 'occurrence'",
    path: ["occurrenceDate"],
  });
export type DeleteEventRequest = z.infer<typeof DeleteEventRequest>;

/** Replaces the invite list wholesale — simpler to reason about than
 * add/remove deltas, and existing RSVPs for retained users are preserved. */
export const InviteEventRequest = z.object({ attendeeUserIds: z.array(EntityId).max(200) });
export type InviteEventRequest = z.infer<typeof InviteEventRequest>;

export const RsvpEventRequest = z.object({ status: AttendeeStatus });
export type RsvpEventRequest = z.infer<typeof RsvpEventRequest>;

/**
 * GET /workspaces/:workspaceId/events. `from`/`to` are required — there is no
 * "all events" read, because answering it would mean expanding every series
 * over an unbounded horizon.
 */
export const ListEventsQuery = z
  .object({
    from: z.string().datetime(),
    to: z.string().datetime(),
    channelId: EntityId.optional(),
    // NOT z.coerce.boolean() — that maps every non-empty string, "false"
    // included, to true (the trap already fixed in tasks-http.ts).
    mine: z
      .enum(["true", "false", "1", "0"])
      .transform((v) => v === "true" || v === "1")
      .optional(),
  })
  .refine((v) => new Date(v.to) > new Date(v.from), { message: "to must be after from", path: ["to"] })
  .refine(
    (v) => new Date(v.to).getTime() - new Date(v.from).getTime() <= MAX_EVENT_WINDOW_DAYS * 86_400_000,
    { message: `window must not exceed ${MAX_EVENT_WINDOW_DAYS} days`, path: ["to"] },
  );
export type ListEventsQuery = z.infer<typeof ListEventsQuery>;
