import { GlobeAltIcon, GlobeAmericasIcon } from "@heroicons/react/20/solid";
import { useRouteLoaderData } from "@remix-run/react";
import { formatDistanceToNow } from "date-fns";
import { Laptop } from "lucide-react";
import { memo, type ReactNode, useEffect, useMemo, useState, useSyncExternalStore } from "react";
import { CopyButton } from "./CopyButton";
import { useLocales } from "./LocaleProvider";
import { Paragraph } from "./Paragraph";
import { SimpleTooltip } from "./Tooltip";

// Cache the browser's local timezone - resolved once and reused
let cachedLocalTimeZone: string | null = null;

function getLocalTimeZone(): string {
  if (cachedLocalTimeZone === null) {
    cachedLocalTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
  }
  return cachedLocalTimeZone;
}

// For SSR compatibility: returns "UTC" on server, actual timezone on client
function subscribeToTimeZone() {
  // No-op - timezone doesn't change
  return () => {};
}

function getTimeZoneSnapshot(): string {
  return getLocalTimeZone();
}

function getServerTimeZoneSnapshot(): string {
  return "UTC";
}

/**
 * Hook to get the browser's local timezone.
 * Uses useSyncExternalStore for SSR compatibility - returns "UTC" on server,
 * actual timezone on client. The timezone is cached and only resolved once.
 */
export function useLocalTimeZone(): string {
  return useSyncExternalStore(subscribeToTimeZone, getTimeZoneSnapshot, getServerTimeZoneSnapshot);
}

/**
 * Hook to get the user's preferred timezone.
 * Returns the timezone stored in the user's preferences cookie (from root loader),
 * falling back to the browser's local timezone if not set.
 */
export function useUserTimeZone(): string {
  const rootData = useRouteLoaderData("root") as { timezone?: string } | undefined;
  const localTimeZone = useLocalTimeZone();
  // Use stored timezone from cookie, or fall back to browser's local timezone
  return rootData?.timezone && rootData.timezone !== "UTC" ? rootData.timezone : localTimeZone;
}

type DateTimeProps = {
  date: Date | string;
  timeZone?: string;
  includeSeconds?: boolean;
  includeTime?: boolean;
  includeDate?: boolean;
  showTimezone?: boolean;
  showTooltip?: boolean;
  hideDate?: boolean;
  previousDate?: Date | string | null; // Add optional previous date for comparison
  hour12?: boolean;
};

export const DateTime = ({
  date,
  timeZone,
  includeSeconds = true,
  includeTime = true,
  includeDate = true,
  showTimezone = false,
  showTooltip = true,
  hour12 = true,
}: DateTimeProps) => {
  const locales = useLocales();
  const userTimeZone = useUserTimeZone();

  const realDate = useMemo(() => (typeof date === "string" ? new Date(date) : date), [date]);

  const formattedDateTime = (
    <span suppressHydrationWarning>
      {formatDateTime(
        realDate,
        timeZone ?? userTimeZone,
        locales,
        includeSeconds,
        includeTime,
        includeDate,
        hour12
      ).replace(/\s/g, String.fromCharCode(32))}
      {showTimezone ? ` (${timeZone ?? "UTC"})` : null}
    </span>
  );

  if (!showTooltip) return formattedDateTime;

  return (
    <SimpleTooltip
      button={formattedDateTime}
      content={
        <TooltipContent
          realDate={realDate}
          timeZone={timeZone}
          localTimeZone={userTimeZone}
          locales={locales}
        />
      }
      side="right"
      asChild={true}
    />
  );
};

export function formatDateTime(
  date: Date,
  timeZone: string,
  locales: string[],
  includeSeconds: boolean,
  includeTime: boolean,
  includeDate: boolean = true,
  hour12: boolean = true
): string {
  return new Intl.DateTimeFormat(locales, {
    year: includeDate ? "numeric" : undefined,
    month: includeDate ? "short" : undefined,
    day: includeDate ? "numeric" : undefined,
    hour: includeTime ? "numeric" : undefined,
    minute: includeTime ? "numeric" : undefined,
    second: includeTime && includeSeconds ? "numeric" : undefined,
    timeZone,
    hour12,
  }).format(date);
}

export function formatDateTimeISO(date: Date, timeZone: string): string {
  // Special handling for UTC
  if (timeZone === "UTC") {
    return date.toISOString();
  }

  // Get the date parts in the target timezone
  const dateFormatter = new Intl.DateTimeFormat("en-US", {
    timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false,
  });

  // Get the timezone offset for this specific date
  const timeZoneFormatter = new Intl.DateTimeFormat("en-US", {
    timeZone,
    timeZoneName: "longOffset",
  });

  const dateParts = Object.fromEntries(
    dateFormatter.formatToParts(date).map(({ type, value }) => [type, value])
  );

  const timeZoneParts = timeZoneFormatter.formatToParts(date);
  const offset =
    timeZoneParts.find((part) => part.type === "timeZoneName")?.value.replace("GMT", "") ||
    "+00:00";

  // Format: YYYY-MM-DDThh:mm:ss.sss±hh:mm
  return (
    `${dateParts.year}-${dateParts.month}-${dateParts.day}T` +
    `${dateParts.hour}:${dateParts.minute}:${dateParts.second}.${String(
      date.getMilliseconds()
    ).padStart(3, "0")}${offset}`
  );
}

/**
 * Human-readable UTC offset for a timezone at a specific instant, e.g. "(UTC +3)".
 * Returns "" for UTC. The offset must be derived from the same (date, timeZone) pair
 * used to render the displayed time so the label always matches the value shown — and
 * so it stays correct across DST boundaries regardless of the viewer's current season.
 */
export function formatUtcOffset(date: Date, timeZone: string): string {
  if (timeZone === "UTC") return "";

  const parts = new Intl.DateTimeFormat("en-US", {
    timeZone,
    timeZoneName: "longOffset",
  }).formatToParts(date);

  // longOffset yields "GMT+03:00", "GMT-08:00", "GMT+05:30", or "GMT" for UTC-equivalent zones.
  const raw = parts.find((part) => part.type === "timeZoneName")?.value.replace("GMT", "") ?? "";
  const match = raw.match(/^([+-])(\d{2}):(\d{2})$/);
  if (!match) return "(UTC +0)";

  const [, sign, hh, mm] = match;
  const hours = parseInt(hh, 10);
  const minutes = parseInt(mm, 10);
  return `(UTC ${sign}${hours}${minutes ? `:${minutes.toString().padStart(2, "0")}` : ""})`;
}

// New component that only shows date when it changes
export const SmartDateTime = ({ date, previousDate = null, hour12 = true }: DateTimeProps) => {
  const locales = useLocales();
  const userTimeZone = useUserTimeZone();
  const realDate = typeof date === "string" ? new Date(date) : date;
  const realPrevDate = previousDate
    ? typeof previousDate === "string"
      ? new Date(previousDate)
      : previousDate
    : null;

  // Check if we should show the date
  const showDatePart = !realPrevDate || !isSameDay(realDate, realPrevDate);

  // Format with appropriate function
  const formattedDateTime = showDatePart
    ? formatSmartDateTime(realDate, userTimeZone, locales, hour12)
    : formatTimeOnly(realDate, userTimeZone, locales, hour12);

  return (
    <span suppressHydrationWarning>
      {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
    </span>
  );
};

// Helper function to check if two dates are on the same day
function isSameDay(date1: Date, date2: Date): boolean {
  return (
    date1.getFullYear() === date2.getFullYear() &&
    date1.getMonth() === date2.getMonth() &&
    date1.getDate() === date2.getDate()
  );
}

// Format with date and time
function formatSmartDateTime(
  date: Date,
  timeZone: string,
  locales: string[],
  hour12: boolean = true
): string {
  return new Intl.DateTimeFormat(locales, {
    month: "short",
    day: "numeric",
    hour: "numeric",
    minute: "numeric",
    second: "numeric",
    timeZone,
    // @ts-ignore fractionalSecondDigits works in most modern browsers
    fractionalSecondDigits: 3,
    hour12,
  }).format(date);
}

// Format time only
function formatTimeOnly(
  date: Date,
  timeZone: string,
  locales: string[],
  hour12: boolean = true
): string {
  return new Intl.DateTimeFormat(locales, {
    hour: "2-digit",
    minute: "numeric",
    second: "numeric",
    timeZone,
    // @ts-ignore fractionalSecondDigits works in most modern browsers
    fractionalSecondDigits: 3,
    hour12,
  }).format(date);
}

const DateTimeAccurateInner = ({
  date,
  timeZone,
  previousDate = null,
  showTooltip = true,
  hideDate = false,
  hour12 = true,
}: DateTimeProps) => {
  const locales = useLocales();
  const userTimeZone = useUserTimeZone();
  // Use provided timeZone prop if available, otherwise fall back to user's preferred timezone
  const displayTimeZone = timeZone ?? userTimeZone;
  const realDate = typeof date === "string" ? new Date(date) : date;
  const realPrevDate = previousDate
    ? typeof previousDate === "string"
      ? new Date(previousDate)
      : previousDate
    : null;

  // Smart formatting based on whether date changed
  const formattedDateTime = useMemo(() => {
    return hideDate
      ? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
      : realPrevDate
        ? isSameDay(realDate, realPrevDate)
          ? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
          : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12)
        : formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12);
  }, [realDate, displayTimeZone, locales, hour12, hideDate, previousDate]);

  if (!showTooltip)
    return (
      <span suppressHydrationWarning>
        {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
      </span>
    );

  const tooltipContent = (
    <TooltipContent
      realDate={realDate}
      timeZone={timeZone}
      localTimeZone={userTimeZone}
      locales={locales}
    />
  );

  return (
    <SimpleTooltip
      button={
        <span suppressHydrationWarning>
          {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
        </span>
      }
      content={tooltipContent}
      side="right"
      asChild={true}
    />
  );
};

function areDateTimePropsEqual(prev: DateTimeProps, next: DateTimeProps): boolean {
  // Compare Date objects by timestamp value, not reference
  const prevTime = prev.date instanceof Date ? prev.date.getTime() : prev.date;
  const nextTime = next.date instanceof Date ? next.date.getTime() : next.date;
  if (prevTime !== nextTime) return false;

  const prevPrevTime =
    prev.previousDate instanceof Date ? prev.previousDate.getTime() : prev.previousDate;
  const nextPrevTime =
    next.previousDate instanceof Date ? next.previousDate.getTime() : next.previousDate;
  if (prevPrevTime !== nextPrevTime) return false;

  return (
    prev.timeZone === next.timeZone &&
    prev.showTooltip === next.showTooltip &&
    prev.hideDate === next.hideDate &&
    prev.hour12 === next.hour12
  );
}

export const DateTimeAccurate = memo(DateTimeAccurateInner, areDateTimePropsEqual);

function formatDateTimeAccurate(
  date: Date,
  timeZone: string,
  locales: string[],
  hour12: boolean = true
): string {
  const datePart = new Intl.DateTimeFormat(locales, {
    month: "short",
    day: "numeric",
    timeZone,
  }).format(date);

  const timePart = new Intl.DateTimeFormat(locales, {
    hour: "numeric",
    minute: "numeric",
    second: "numeric",
    timeZone,
    // @ts-ignore fractionalSecondDigits works in most modern browsers
    fractionalSecondDigits: 3,
    hour12,
  }).format(date);

  return `${datePart} ${timePart}`;
}

type RelativeDateTimeProps = {
  date: Date | string;
  timeZone?: string;
  capitalize?: boolean;
};

function getRelativeText(date: Date, capitalize = true): string {
  const text = formatDistanceToNow(date, { addSuffix: true });
  if (!capitalize) return text;
  return text.charAt(0).toUpperCase() + text.slice(1);
}

export const RelativeDateTime = ({ date, timeZone, capitalize = true }: RelativeDateTimeProps) => {
  const locales = useLocales();
  const userTimeZone = useUserTimeZone();

  const realDate = useMemo(() => (typeof date === "string" ? new Date(date) : date), [date]);

  const [relativeText, setRelativeText] = useState(() => getRelativeText(realDate, capitalize));

  // Every 60s refresh
  useEffect(() => {
    const interval = setInterval(() => {
      setRelativeText(getRelativeText(realDate, capitalize));
    }, 60_000);
    return () => clearInterval(interval);
  }, [realDate, capitalize]);

  // On first render
  useEffect(() => {
    setRelativeText(getRelativeText(realDate, capitalize));
  }, [realDate, capitalize]);

  return (
    <SimpleTooltip
      button={<span suppressHydrationWarning>{relativeText}</span>}
      content={
        <TooltipContent
          realDate={realDate}
          timeZone={timeZone}
          localTimeZone={userTimeZone}
          locales={locales}
        />
      }
      side="right"
      asChild={true}
    />
  );
};

export const DateTimeShort = ({ date, hour12 = true }: DateTimeProps) => {
  const locales = useLocales();
  const userTimeZone = useUserTimeZone();
  const realDate = typeof date === "string" ? new Date(date) : date;
  const formattedDateTime = formatDateTimeShort(realDate, userTimeZone, locales, hour12);

  return (
    <span suppressHydrationWarning>
      {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
    </span>
  );
};

function formatDateTimeShort(
  date: Date,
  timeZone: string,
  locales: string[],
  hour12: boolean = true
): string {
  const formattedDateTime = new Intl.DateTimeFormat(locales, {
    hour: "numeric",
    minute: "numeric",
    second: "numeric",
    timeZone,
    // @ts-ignore fractionalSecondDigits works in most modern browsers
    fractionalSecondDigits: 3,
    hour12,
  }).format(date);

  return formattedDateTime;
}

type DateTimeTooltipContentProps = {
  title: string;
  dateTime: string;
  isoDateTime: string;
  icon: ReactNode;
  offset?: string;
};

function DateTimeTooltipContent({
  title,
  dateTime,
  isoDateTime,
  icon,
  offset,
}: DateTimeTooltipContentProps) {
  return (
    <div className="flex flex-col gap-1">
      <div className="flex items-center gap-1 text-sm">
        {icon}
        <span className="font-medium">{title}</span>
        {offset ? <span className="font-normal text-text-dimmed">{offset}</span> : null}
      </div>
      <div className="flex items-center justify-between gap-2">
        <Paragraph variant="extra-small" className="text-text-dimmed">
          {dateTime}
        </Paragraph>
        <CopyButton value={isoDateTime} variant="icon" size="extra-small" showTooltip={false} />
      </div>
    </div>
  );
}

function TooltipContent({
  realDate,
  timeZone,
  localTimeZone,
  locales,
}: {
  realDate: Date;
  timeZone?: string;
  localTimeZone: string;
  locales: string[];
}) {
  return (
    <div className="flex flex-col gap-1">
      <div className="flex flex-col gap-2.5 pb-1">
        {timeZone && timeZone !== "UTC" && (
          <DateTimeTooltipContent
            title={timeZone}
            dateTime={formatDateTime(realDate, timeZone, locales, true, true, true)}
            isoDateTime={formatDateTimeISO(realDate, timeZone)}
            icon={<GlobeAmericasIcon className="size-4 text-purple-500" />}
          />
        )}
        <DateTimeTooltipContent
          title="UTC"
          dateTime={formatDateTime(realDate, "UTC", locales, true, true, true)}
          isoDateTime={formatDateTimeISO(realDate, "UTC")}
          icon={<GlobeAltIcon className="size-4 text-blue-500" />}
        />
        <DateTimeTooltipContent
          title="Local"
          dateTime={formatDateTime(realDate, localTimeZone, locales, true, true, true)}
          isoDateTime={formatDateTimeISO(realDate, localTimeZone)}
          icon={<Laptop className="size-4 text-green-500" />}
          offset={formatUtcOffset(realDate, localTimeZone)}
        />
      </div>
    </div>
  );
}
