/** Default guild timezone (matches Prisma `GuildSettings.timezone`). */
export const DEFAULT_GUILD_TIMEZONE = 'Europe/Berlin';

export function isValidTimezone(timezone: string): boolean {
  try {
    Intl.DateTimeFormat(undefined, { timeZone: timezone });
    return true;
  } catch {
    return false;
  }
}

type ZonedParts = {
  year: number;
  month: number;
  day: number;
  hour: number;
  minute: number;
  second: number;
};

function getZonedParts(date: Date, timeZone: string): ZonedParts {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hourCycle: 'h23'
  });

  const map: Partial<Record<Intl.DateTimeFormatPartTypes, string>> = {};
  for (const part of formatter.formatToParts(date)) {
    if (part.type !== 'literal') {
      map[part.type] = part.value;
    }
  }

  return {
    year: Number(map.year),
    month: Number(map.month),
    day: Number(map.day),
    hour: Number(map.hour),
    minute: Number(map.minute),
    second: Number(map.second)
  };
}

const NAIVE_LOCAL_RE = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/;

/**
 * Interprets a wall-clock datetime (`YYYY-MM-DDTHH:mm` / with seconds / space
 * separator) as being in `timeZone` and returns the corresponding UTC `Date`.
 */
export function guildLocalDateTimeToUtc(localDateTime: string, timeZone: string): Date {
  const match = localDateTime.trim().match(NAIVE_LOCAL_RE);
  if (!match) {
    throw new RangeError('invalid_local_datetime');
  }
  if (!isValidTimezone(timeZone)) {
    throw new RangeError('invalid_timezone');
  }

  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  const hour = Number(match[4]);
  const minute = Number(match[5]);
  const second = Number(match[6] ?? 0);

  // Initial guess: treat the components as UTC, then correct by the TZ offset.
  let utcMs = Date.UTC(year, month - 1, day, hour, minute, second);
  for (let i = 0; i < 3; i += 1) {
    const parts = getZonedParts(new Date(utcMs), timeZone);
    const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
    const targetAsUtc = Date.UTC(year, month - 1, day, hour, minute, second);
    const diff = targetAsUtc - asUtc;
    if (diff === 0) {
      break;
    }
    utcMs += diff;
  }

  return new Date(utcMs);
}

/**
 * Resolves a schedule `runAt` input to a UTC instant.
 * - Values with `Z` / numeric offset are treated as absolute.
 * - Naive `datetime-local` values are interpreted in the guild timezone.
 */
export function resolveScheduleRunAt(input: string, timeZone: string): Date {
  const trimmed = input.trim();
  if (!trimmed) {
    throw new RangeError('invalid_when');
  }

  if (/([zZ]|[+-]\d{2}:?\d{2})$/.test(trimmed)) {
    const parsed = new Date(trimmed);
    if (Number.isNaN(parsed.getTime())) {
      throw new RangeError('invalid_when');
    }
    return parsed;
  }

  if (NAIVE_LOCAL_RE.test(trimmed)) {
    return guildLocalDateTimeToUtc(trimmed, timeZone);
  }

  const parsed = new Date(trimmed);
  if (Number.isNaN(parsed.getTime())) {
    throw new RangeError('invalid_when');
  }
  return parsed;
}
