defmodule ToweropsWeb.TimeHelpers do
  @moduledoc """
  Helper functions for formatting time and date values with timezone support.

  All functions use a centralized `to_user_timezone/2` function to convert UTC
  datetimes to the user's preferred timezone.
  """

  use Gettext, backend: ToweropsWeb.Gettext

  # Common timezone offsets in seconds
  # Note: These are standard time offsets. DST handling would require a full timezone database.
  @timezone_offsets %{
    "UTC" => 0,
    "America/New_York" => -18_000,
    # EST (UTC-5)
    "America/Chicago" => -21_600,
    # CST (UTC-6)
    "America/Denver" => -25_200,
    # MST (UTC-7)
    "America/Los_Angeles" => -28_800,
    # PST (UTC-8)
    "America/Phoenix" => -25_200,
    # MST (no DST)
    "Europe/London" => 0,
    # GMT (UTC+0)
    "Europe/Paris" => 3600,
    # CET (UTC+1)
    "Europe/Berlin" => 3600,
    # CET (UTC+1)
    "Asia/Tokyo" => 32_400,
    # JST (UTC+9)
    "Asia/Shanghai" => 28_800,
    # CST (UTC+8)
    "Asia/Dubai" => 14_400,
    # GST (UTC+4)
    "Australia/Sydney" => 36_000
    # AEST (UTC+10)
  }

  @timezone_abbrs %{
    "UTC" => "UTC",
    "America/New_York" => "EST",
    "America/Chicago" => "CST",
    "America/Denver" => "MST",
    "America/Los_Angeles" => "PST",
    "America/Phoenix" => "MST",
    "Europe/London" => "GMT",
    "Europe/Paris" => "CET",
    "Europe/Berlin" => "CET",
    "Asia/Tokyo" => "JST",
    "Asia/Shanghai" => "CST",
    "Asia/Dubai" => "GST",
    "Australia/Sydney" => "AEST"
  }

  @doc """
  Central function to convert a UTC DateTime to the user's timezone.

  All other formatting functions should use this function to ensure consistent
  timezone handling across the application.

  Returns `{:ok, shifted_datetime, timezone_abbr}` or `{:error, reason}`.

  ## Examples

      iex> datetime = ~U[2026-01-15 14:34:00Z]
      iex> to_user_timezone(datetime, "America/New_York")
      {:ok, ~U[2026-01-15 09:34:00Z], "EST"}
  """
  @spec to_user_timezone(DateTime.t(), String.t()) ::
          {:ok, DateTime.t(), String.t()} | {:error, atom()}
  def to_user_timezone(datetime, timezone) when is_binary(timezone) do
    case Map.get(@timezone_offsets, timezone) do
      nil ->
        {:error, :unknown_timezone}

      offset_seconds ->
        shifted_dt = DateTime.add(datetime, offset_seconds, :second)
        tz_abbr = Map.get(@timezone_abbrs, timezone, timezone)
        {:ok, shifted_dt, tz_abbr}
    end
  end

  @doc """
  Formats a DateTime into a human-readable "time ago" string.

  Supports short time periods (seconds, minutes, hours, days) and
  long time periods (months, years) for things like build timestamps.

  Handles clock skew gracefully by treating future timestamps as "just now".

  ## Examples

      iex> datetime = DateTime.add(DateTime.utc_now(), -65, :second)
      iex> ToweropsWeb.TimeHelpers.format_time_ago(datetime)
      "1m ago"

      iex> datetime = DateTime.add(DateTime.utc_now(), -7200, :second)
      iex> ToweropsWeb.TimeHelpers.format_time_ago(datetime)
      "2h ago"

  """
  @spec format_time_ago(DateTime.t() | nil) :: String.t()
  def format_time_ago(nil), do: gettext("Never")

  def format_time_ago(datetime) do
    now = DateTime.utc_now()
    diff = DateTime.diff(now, datetime, :second)

    # Clamp diff to 0 if negative (handles clock skew where datetime is in the future)
    diff = max(diff, 0)

    cond do
      diff < 60 ->
        ngettext("%{count}s ago", "%{count}s ago", diff, count: diff)

      diff < 3600 ->
        minutes = div(diff, 60)
        ngettext("%{count}m ago", "%{count}m ago", minutes, count: minutes)

      diff < 86_400 ->
        hours = div(diff, 3600)
        ngettext("%{count}h ago", "%{count}h ago", hours, count: hours)

      diff < 2_592_000 ->
        days = div(diff, 86_400)
        ngettext("%{count}d ago", "%{count}d ago", days, count: days)

      diff < 31_536_000 ->
        months = div(diff, 2_592_000)
        ngettext("%{count}mo ago", "%{count}mo ago", months, count: months)

      true ->
        years = div(diff, 31_536_000)
        ngettext("%{count}y ago", "%{count}y ago", years, count: years)
    end
  end

  @doc """
  Formats a DateTime into a full date and time string in UTC with default 12h format.
  """
  @spec format_datetime(DateTime.t() | nil) :: String.t()
  def format_datetime(datetime), do: format_datetime(datetime, "UTC", "24h")

  @doc """
  Formats a DateTime into a full date and time string, converted to the given timezone.

  ## Examples

      iex> datetime = ~U[2026-01-15 14:34:00Z]
      iex> ToweropsWeb.TimeHelpers.format_datetime(datetime, "America/New_York", "12h")
      "Jan 15, 2026 at 09:34 AM EST"

      iex> datetime = ~U[2026-01-15 14:34:00Z]
      iex> ToweropsWeb.TimeHelpers.format_datetime(datetime, "UTC", "24h")
      "Jan 15, 2026 at 14:34 UTC"

  """
  @spec format_datetime(DateTime.t() | nil, String.t() | nil, String.t()) :: String.t()
  def format_datetime(nil, _timezone, _time_format), do: gettext("Never")

  def format_datetime(datetime, nil, time_format), do: format_datetime(datetime, "UTC", time_format)

  def format_datetime(datetime, timezone, time_format) when is_binary(timezone) do
    case to_user_timezone(datetime, timezone) do
      {:ok, shifted_dt, tz_abbr} ->
        time_str = format_time_part(shifted_dt, time_format)
        Calendar.strftime(shifted_dt, "%b %d, %Y at #{time_str} #{tz_abbr}")

      {:error, _reason} ->
        # Fallback to UTC if timezone is invalid
        time_str = format_time_part(datetime, time_format)
        Calendar.strftime(datetime, "%b %d, %Y at #{time_str} UTC")
    end
  end

  # Backwards compatibility - accepts 2 args, defaults to 24h format
  def format_datetime(datetime, timezone) do
    format_datetime(datetime, timezone, "24h")
  end

  @doc """
  Formats a DateTime into a short date string in UTC.
  """
  @spec format_date(DateTime.t() | nil) :: String.t()
  def format_date(datetime), do: format_date(datetime, "UTC")

  @doc """
  Formats a DateTime into a short date string, converted to the given timezone.

  ## Examples

      iex> datetime = ~U[2026-01-15 14:34:00Z]
      iex> ToweropsWeb.TimeHelpers.format_date(datetime, "UTC")
      "Jan 15, 2026"

      iex> datetime = ~U[2026-01-15 02:00:00Z]
      iex> ToweropsWeb.TimeHelpers.format_date(datetime, "America/Los_Angeles")
      "Jan 14, 2026"

  """
  @spec format_date(DateTime.t() | nil, String.t() | nil) :: String.t()
  def format_date(nil, _timezone), do: gettext("Never")

  def format_date(datetime, nil), do: format_date(datetime, "UTC")

  def format_date(datetime, timezone) when is_binary(timezone) do
    case to_user_timezone(datetime, timezone) do
      {:ok, shifted_dt, _tz_abbr} ->
        Calendar.strftime(shifted_dt, "%b %d, %Y")

      {:error, _reason} ->
        # Fallback to UTC if timezone is invalid
        Calendar.strftime(datetime, "%b %d, %Y")
    end
  end

  @doc """
  Formats a DateTime into ISO 8601 format in UTC with default 24h format.
  """
  @spec format_iso8601(DateTime.t() | nil) :: String.t()
  def format_iso8601(datetime), do: format_iso8601(datetime, "UTC", "24h")

  @doc """
  Formats a DateTime into ISO 8601 format, converted to the given timezone.

  ## Examples

      iex> datetime = ~U[2026-01-15 14:34:00Z]
      iex> ToweropsWeb.TimeHelpers.format_iso8601(datetime, "UTC", "24h")
      "2026-01-15 14:34:00 UTC"

      iex> datetime = ~U[2026-01-15 14:34:00Z]
      iex> ToweropsWeb.TimeHelpers.format_iso8601(datetime, "America/New_York", "12h")
      "2026-01-15 09:34:00 AM EST"

  """
  @spec format_iso8601(DateTime.t() | nil, String.t() | nil, String.t()) :: String.t()
  def format_iso8601(nil, _timezone, _time_format), do: gettext("Never")

  def format_iso8601(datetime, nil, time_format), do: format_iso8601(datetime, "UTC", time_format)

  def format_iso8601(datetime, timezone, time_format) when is_binary(timezone) do
    case to_user_timezone(datetime, timezone) do
      {:ok, shifted_dt, tz_abbr} ->
        time_str = format_time_part(shifted_dt, time_format)
        Calendar.strftime(shifted_dt, "%Y-%m-%d #{time_str} #{tz_abbr}")

      {:error, _reason} ->
        # Fallback to UTC if timezone is invalid
        time_str = format_time_part(datetime, time_format)
        Calendar.strftime(datetime, "%Y-%m-%d #{time_str} UTC")
    end
  end

  # Backwards compatibility - accepts 2 args, defaults to 24h format for ISO8601
  def format_iso8601(datetime, timezone) do
    format_iso8601(datetime, timezone, "24h")
  end

  @doc """
  Formats a UTC hour (0-23) into the user's timezone with abbreviation.

  Uses 12-hour format with AM/PM.

  ## Examples

      iex> format_utc_hour(7, "America/New_York")
      "2:00 AM EST"

      iex> format_utc_hour(7, "UTC")
      "7:00 AM UTC"
  """
  def format_utc_hour(utc_hour, timezone \\ "UTC", time_format \\ "12h") when utc_hour >= 0 and utc_hour <= 23 do
    # Create a DateTime for today at the specified UTC hour
    today = Date.utc_today()
    {:ok, datetime} = DateTime.new(today, ~T[00:00:00], "Etc/UTC")
    datetime = DateTime.add(datetime, utc_hour * 3600, :second)

    case to_user_timezone(datetime, timezone) do
      {:ok, shifted_dt, tz_abbr} ->
        format_hour(shifted_dt.hour, tz_abbr, time_format)

      {:error, _reason} ->
        format_hour(utc_hour, "UTC", time_format)
    end
  end

  defp format_hour(hour, tz_abbr, "24h") do
    "#{hour}:00 #{tz_abbr}"
  end

  defp format_hour(hour, tz_abbr, _time_format) do
    {hour_12, am_pm} = to_12hour_format(hour)
    "#{hour_12}:00 #{am_pm} #{tz_abbr}"
  end

  @doc """
  Formats a DateTime using timezone and time_format from a scope.

  Convenience function that extracts timezone and time_format from a scope map
  (or any map with `:timezone` and `:time_format` keys).

  ## Examples

      iex> scope = %{timezone: "America/New_York", time_format: "12h"}
      iex> ToweropsWeb.TimeHelpers.format_datetime_for_scope(~U[2026-01-15 14:34:00Z], scope)
      "Jan 15, 2026 at 09:34 AM EST"

  """
  def format_datetime_for_scope(datetime, scope) do
    format_datetime(datetime, scope.timezone, scope.time_format)
  end

  # Private helper to format the time portion based on user preference
  defp format_time_part(datetime, "24h") do
    Calendar.strftime(datetime, "%H:%M:%S")
  end

  defp format_time_part(datetime, _default_12h) do
    Calendar.strftime(datetime, "%I:%M %p")
  end

  # Private helper to convert 24-hour format to 12-hour format with AM/PM
  defp to_12hour_format(hour) when hour == 0, do: {12, "AM"}
  defp to_12hour_format(hour) when hour < 12, do: {hour, "AM"}
  defp to_12hour_format(hour) when hour == 12, do: {12, "PM"}
  defp to_12hour_format(hour), do: {hour - 12, "PM"}
end
