import DateObject from "react-date-object";
import persian from "react-date-object/calendars/persian";
import gregorian from "react-date-object/calendars/gregorian";

export type JalaliDateParts = {
  year: number;
  month: number;
  day: number;
};

export const TEHRAN_TIMEZONE = "Asia/Tehran";

const PERSIAN_MONTHS = [
  "فروردین",
  "اردیبهشت",
  "خرداد",
  "تیر",
  "مرداد",
  "شهریور",
  "مهر",
  "آبان",
  "آذر",
  "دی",
  "بهمن",
  "اسفند",
];

const PERSIAN_WEEKDAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"];

function div(a: number, b: number) {
  return Math.floor(a / b);
}

function mod(a: number, b: number) {
  return a - Math.floor(a / b) * b;
}

function pad(value: number) {
  return String(value).padStart(2, "0");
}

function createGregorianDateObject(year: number, month: number, day: number) {
  return new DateObject({
    calendar: gregorian,
    year,
    month,
    day,
  });
}

function createPersianDateObject(year: number, month: number, day: number) {
  return new DateObject({
    calendar: persian,
    year,
    month,
    day,
  });
}

function parseGmtOffset(offsetLabel: string) {
  const normalized = offsetLabel.replace("GMT", "");
  const sign = normalized.startsWith("-") ? -1 : 1;
  const [hoursPart, minutesPart = "0"] = normalized.replace(/[+-]/, "").split(":");
  return sign * (Number(hoursPart) * 60 + Number(minutesPart));
}

function getTimeZoneOffsetMinutes(date: Date, timeZone: string) {
  const formatter = new Intl.DateTimeFormat("en-US", {
    timeZone,
    timeZoneName: "shortOffset",
    hour: "2-digit",
  });
  const offsetPart = formatter.formatToParts(date).find((part) => part.type === "timeZoneName")?.value ?? "GMT+0";
  return parseGmtOffset(offsetPart);
}

function getTimeZoneDateParts(date: Date, timeZone: string) {
  const formatter = new Intl.DateTimeFormat("en-CA", {
    timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    hourCycle: "h23",
  });

  const parts = formatter.formatToParts(date);
  const read = (type: string) => Number(parts.find((part) => part.type === type)?.value ?? "0");

  return {
    year: read("year"),
    month: read("month"),
    day: read("day"),
    hour: read("hour"),
    minute: read("minute"),
  };
}

function zonedDateTimeToUtcDate(
  year: number,
  month: number,
  day: number,
  hour: number,
  minute: number,
  timeZone = TEHRAN_TIMEZONE
) {
  let utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0, 0));

  for (let index = 0; index < 2; index += 1) {
    const offsetMinutes = getTimeZoneOffsetMinutes(utcDate, timeZone);
    utcDate = new Date(Date.UTC(year, month - 1, day, hour, minute, 0, 0) - offsetMinutes * 60 * 1000);
  }

  return utcDate;
}

function jalCal(jy: number) {
  const breaks = [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178];
  const bl = breaks.length;

  if (jy < breaks[0] || jy >= breaks[bl - 1]) {
    throw new Error("Invalid Jalali year");
  }

  let jump = 0;
  let leapJ = -14;
  let jp = breaks[0];

  for (let i = 1; i < bl; i += 1) {
    const jm = breaks[i];
    jump = jm - jp;
    if (jy < jm) {
      break;
    }
    leapJ += div(jump, 33) * 8 + div(mod(jump, 33), 4);
    jp = jm;
  }

  let n = jy - jp;
  leapJ += div(n, 33) * 8 + div(mod(n, 33) + 3, 4);

  if (mod(jump, 33) === 4 && jump - n === 4) {
    leapJ += 1;
  }

  const gy = jy + 621;
  const leapG = div(gy, 4) - div((div(gy, 100) + 1) * 3, 4) - 150;
  const march = 20 + leapJ - leapG;

  if (jump - n < 6) {
    n = n - jump + div(jump + 4, 33) * 33;
  }

  let leap = mod(mod(n + 1, 33) - 1, 4);
  if (leap === -1) {
    leap = 4;
  }

  return { leap, gy, march };
}

function g2d(gy: number, gm: number, gd: number) {
  let d =
    div((gy + div(gm - 8, 6) + 100100) * 1461, 4) +
    div(153 * mod(gm + 9, 12) + 2, 5) +
    gd -
    34840408;
  d = d - div(div(gy + 100100 + div(gm - 8, 6), 100) * 3, 4) + 752;
  return d;
}

function d2g(jdn: number) {
  let j = 4 * jdn + 139361631;
  j = j + div(div(4 * jdn + 183187720, 146097) * 3, 4) * 4 - 3908;
  const i = div(mod(j, 1461), 4) * 5 + 308;
  const gd = div(mod(i, 153), 5) + 1;
  const gm = mod(div(i, 153), 12) + 1;
  const gy = div(j, 1461) - 100100 + div(8 - gm, 6);
  return { year: gy, month: gm, day: gd };
}

function j2d(jy: number, jm: number, jd: number) {
  const r = jalCal(jy);
  return g2d(r.gy, 3, r.march) + (jm - 1) * 31 - div(jm, 7) * (jm - 7) + jd - 1;
}

function d2j(jdn: number): JalaliDateParts {
  const g = d2g(jdn);
  let jy = g.year - 621;
  const r = jalCal(jy);
  const jdn1f = g2d(g.year, 3, r.march);
  let k = jdn - jdn1f;

  if (k >= 0) {
    if (k <= 185) {
      return {
        year: jy,
        month: 1 + div(k, 31),
        day: mod(k, 31) + 1,
      };
    }
    k -= 186;
  } else {
    jy -= 1;
    k += 179;
    if (r.leap === 1) {
      k += 1;
    }
  }

  return {
    year: jy,
    month: 7 + div(k, 30),
    day: mod(k, 30) + 1,
  };
}

export function toJalali(gy: number, gm: number, gd: number) {
  const converted = createGregorianDateObject(gy, gm, gd).convert(persian);
  return {
    year: Number(converted.year),
    month: Number(converted.month.number),
    day: Number(converted.day),
  };
}

export function toGregorian(jy: number, jm: number, jd: number) {
  const converted = createPersianDateObject(jy, jm, jd).convert(gregorian);
  return {
    year: Number(converted.year),
    month: Number(converted.month.number),
    day: Number(converted.day),
  };
}

export function isLeapJalaliYear(year: number) {
  return createPersianDateObject(year, 1, 1).isLeap;
}

export function getJalaliMonthDays(year: number, month: number) {
  if (month <= 6) return 31;
  if (month <= 11) return 30;
  return isLeapJalaliYear(year) ? 30 : 29;
}

export function getPersianMonthName(month: number) {
  return PERSIAN_MONTHS[month - 1];
}

export function getPersianWeekdays() {
  return PERSIAN_WEEKDAYS;
}

export function formatDateOnly(year: number, month: number, day: number) {
  return `${year}-${pad(month)}-${pad(day)}`;
}

export function parseDateOnly(value: string) {
  const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!match) return null;
  return { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]) };
}

export function getGregorianDateInputValue(date: Date) {
  return formatDateOnly(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
}

export function formatGregorianDate(date: Date) {
  return new Intl.DateTimeFormat("en-GB", {
    timeZone: TEHRAN_TIMEZONE,
    weekday: "long",
    year: "numeric",
    month: "long",
    day: "numeric",
  }).format(date);
}

export function formatGregorianDateTime(date: Date) {
  return new Intl.DateTimeFormat("en-GB", {
    timeZone: TEHRAN_TIMEZONE,
    weekday: "long",
    year: "numeric",
    month: "long",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
  }).format(date);
}

export function formatPersianDate(date: Date) {
  return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
    timeZone: TEHRAN_TIMEZONE,
    weekday: "long",
    year: "numeric",
    month: "long",
    day: "numeric",
  }).format(date);
}

export function formatPersianDateTime(date: Date) {
  return new Intl.DateTimeFormat("fa-IR-u-ca-persian", {
    timeZone: TEHRAN_TIMEZONE,
    weekday: "long",
    year: "numeric",
    month: "long",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    hourCycle: "h23",
  }).format(date);
}

export function formatPersianTime(date: Date) {
  return new Intl.DateTimeFormat("fa-IR", {
    timeZone: TEHRAN_TIMEZONE,
    hour: "2-digit",
    minute: "2-digit",
    hourCycle: "h23",
  }).format(date);
}

export function getTehranTimeInputValue(date: Date) {
  const parts = getTimeZoneDateParts(date, TEHRAN_TIMEZONE);
  return `${pad(parts.hour)}:${pad(parts.minute)}`;
}

export function getWeekdayOffset(year: number, month: number, day = 1) {
  const gregorian = toGregorian(year, month, day);
  const jsDay = new Date(Date.UTC(gregorian.year, gregorian.month - 1, gregorian.day)).getUTCDay();
  return (jsDay + 1) % 7;
}

export function getTodayJalali() {
  const nowParts = getTimeZoneDateParts(new Date(), TEHRAN_TIMEZONE);
  return toJalali(nowParts.year, nowParts.month, nowParts.day);
}

export function jalaliDateToGregorianString(year: number, month: number, day: number) {
  const gregorian = toGregorian(year, month, day);
  return formatDateOnly(gregorian.year, gregorian.month, gregorian.day);
}

export function gregorianDateAndTimeToUtcIso(date: string, time: string) {
  const gregorian = parseDateOnly(date);
  if (!gregorian) return "";
  const [hour, minute] = time.split(":").map(Number);
  return zonedDateTimeToUtcDate(
    gregorian.year,
    gregorian.month,
    gregorian.day,
    hour || 0,
    minute || 0
  ).toISOString();
}

export function jalaliDateTimeToUtcIso(year: number, month: number, day: number, time: string) {
  const gregorian = toGregorian(year, month, day);
  const [hour, minute] = time.split(":").map(Number);
  return zonedDateTimeToUtcDate(
    gregorian.year,
    gregorian.month,
    gregorian.day,
    hour || 0,
    minute || 0
  ).toISOString();
}

export function getDateFromJalaliDateTime(year: number, month: number, day: number, time = "12:00") {
  const gregorian = toGregorian(year, month, day);
  const [hour, minute] = time.split(":").map(Number);
  return zonedDateTimeToUtcDate(
    gregorian.year,
    gregorian.month,
    gregorian.day,
    hour || 0,
    minute || 0
  );
}

export function getGregorianDateForDisplay(year: number, month: number, day: number) {
  return formatGregorianDate(getDateFromJalaliDateTime(year, month, day, "12:00"));
}

export function dateValueToJalali(value: string, mode: "date" | "datetime") {
  if (!value) return null;

  if (mode === "date") {
    const parsed = parseDateOnly(value);
    if (!parsed) return null;
    return {
      ...toJalali(parsed.year, parsed.month, parsed.day),
      time: "",
    };
  }

  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return null;

  const tehranParts = getTimeZoneDateParts(date, TEHRAN_TIMEZONE);
  return {
    ...toJalali(tehranParts.year, tehranParts.month, tehranParts.day),
    time: `${pad(tehranParts.hour)}:${pad(tehranParts.minute)}`,
  };
}
