defmodule Berrypod.Newsletter do
  @moduledoc """
  Newsletter subscriber management and campaign sending.

  Privacy-first design: double opt-in, plain text only, no tracking pixels,
  hashed confirmation tokens, minimal data collection (email only).
  """

  import Ecto.Query

  alias Berrypod.Newsletter.{Campaign, Subscriber}
  alias Berrypod.{Repo, Settings}

  # ── Subscriber flow ──────────────────────────────────────────────

  @doc """
  Subscribes an email address. Creates a pending subscriber and enqueues
  a confirmation email.

  Returns `{:ok, subscriber}` for new signups, `{:already_confirmed, subscriber}`
  if the email is already confirmed, or `{:error, changeset}` on validation failure.

  Options:
  - `consent_text` — the exact wording shown at signup
  - `source` — "website" (default) or "admin_import"
  - `ip_hash` — hashed IP for consent proof
  """
  def subscribe(email, opts \\ []) do
    email = email |> String.trim() |> String.downcase()

    case Repo.get_by(Subscriber, email: email) do
      %Subscriber{status: "confirmed"} = sub ->
        {:already_confirmed, sub}

      %Subscriber{status: "unsubscribed"} = sub ->
        # Re-subscribe: reset to pending with new token
        resubscribe(sub, opts)

      %Subscriber{status: "pending"} = sub ->
        # Already pending — resend confirmation
        enqueue_confirmation(sub)
        {:ok, sub}

      nil ->
        create_subscriber(email, opts)
    end
  end

  defp create_subscriber(email, opts) do
    {raw_token, hashed_token} = generate_token()

    attrs = %{
      email: email,
      status: "pending",
      confirmation_token: hashed_token,
      consent_text: Keyword.get(opts, :consent_text),
      source: Keyword.get(opts, :source, "website"),
      ip_hash: Keyword.get(opts, :ip_hash)
    }

    case %Subscriber{} |> Subscriber.changeset(attrs) |> Repo.insert() do
      {:ok, sub} ->
        enqueue_confirmation(%{sub | confirmation_token: raw_token})
        {:ok, sub}

      {:error, changeset} ->
        {:error, changeset}
    end
  end

  defp resubscribe(sub, opts) do
    {raw_token, hashed_token} = generate_token()

    attrs = %{
      status: "pending",
      confirmation_token: hashed_token,
      unsubscribed_at: nil,
      consent_text: Keyword.get(opts, :consent_text) || sub.consent_text,
      ip_hash: Keyword.get(opts, :ip_hash) || sub.ip_hash
    }

    case sub |> Subscriber.changeset(attrs) |> Repo.update() do
      {:ok, updated} ->
        enqueue_confirmation(%{updated | confirmation_token: raw_token})
        {:ok, updated}

      {:error, changeset} ->
        {:error, changeset}
    end
  end

  @doc """
  Confirms a subscriber via their raw confirmation token.
  """
  def confirm(raw_token) do
    hashed = hash_token(raw_token)

    case Repo.get_by(Subscriber, confirmation_token: hashed) do
      %Subscriber{status: "pending"} = sub ->
        sub
        |> Subscriber.changeset(%{
          status: "confirmed",
          confirmed_at: DateTime.utc_now() |> DateTime.truncate(:second),
          confirmation_token: nil
        })
        |> Repo.update()

      %Subscriber{status: "confirmed"} = sub ->
        {:ok, sub}

      _ ->
        {:error, :invalid_token}
    end
  end

  @doc """
  Unsubscribes an email. Updates the subscriber record and adds to the
  global email suppression list.
  """
  def unsubscribe(email) do
    email = email |> String.trim() |> String.downcase()

    case Repo.get_by(Subscriber, email: email) do
      %Subscriber{status: "unsubscribed"} = sub ->
        {:ok, sub}

      %Subscriber{} = sub ->
        sub
        |> Subscriber.changeset(%{
          status: "unsubscribed",
          unsubscribed_at: DateTime.utc_now() |> DateTime.truncate(:second),
          confirmation_token: nil
        })
        |> Repo.update()

      nil ->
        {:error, :not_found}
    end
  end

  # ── Subscriber queries ───────────────────────────────────────────

  @doc "Lists subscribers, optionally filtered by status or email search."
  def list_subscribers(opts \\ []) do
    query =
      from(s in Subscriber, order_by: [desc: s.inserted_at])

    query =
      case Keyword.get(opts, :status) do
        nil -> query
        "all" -> query
        status -> from(s in query, where: s.status == ^status)
      end

    query =
      case Keyword.get(opts, :search) do
        nil -> query
        "" -> query
        term -> from(s in query, where: like(s.email, ^"%#{term}%"))
      end

    Repo.all(query)
  end

  @doc """
  Like `list_subscribers/1` but returns a `%Pagination{}` struct.
  """
  def list_subscribers_paginated(opts \\ []) do
    query = from(s in Subscriber, order_by: [desc: s.inserted_at])

    query =
      case Keyword.get(opts, :status) do
        nil -> query
        "all" -> query
        status -> from(s in query, where: s.status == ^status)
      end

    query =
      case Keyword.get(opts, :search) do
        nil -> query
        "" -> query
        term -> from(s in query, where: like(s.email, ^"%#{term}%"))
      end

    Berrypod.Pagination.paginate(query, page: opts[:page], per_page: opts[:per_page] || 25)
  end

  @doc "Returns subscriber counts grouped by status."
  def count_subscribers_by_status do
    from(s in Subscriber,
      group_by: s.status,
      select: {s.status, count(s.id)}
    )
    |> Repo.all()
    |> Map.new()
  end

  @doc "Returns the count of confirmed subscribers."
  def confirmed_subscriber_count do
    from(s in Subscriber, where: s.status == "confirmed", select: count())
    |> Repo.one()
  end

  def get_subscriber!(id), do: Repo.get!(Subscriber, id)

  @doc "Hard-deletes a subscriber (GDPR right to erasure)."
  def delete_subscriber(%Subscriber{} = sub) do
    Repo.delete(sub)
  end

  @doc "Returns all data held about a subscriber as a plain map (GDPR right to access)."
  def export_subscriber_data(%Subscriber{} = sub) do
    %{
      email: sub.email,
      status: sub.status,
      subscribed_at: sub.inserted_at,
      confirmed_at: sub.confirmed_at,
      unsubscribed_at: sub.unsubscribed_at,
      source: sub.source,
      consent_text: sub.consent_text
    }
  end

  @doc "Exports all subscribers as a CSV string."
  def export_all_subscribers_csv do
    subscribers = list_subscribers()

    header = "email,status,confirmed_at,subscribed_at\n"

    rows =
      Enum.map_join(subscribers, "\n", fn s ->
        [
          s.email,
          s.status,
          format_csv_datetime(s.confirmed_at),
          format_csv_datetime(s.inserted_at)
        ]
        |> Enum.join(",")
      end)

    header <> rows
  end

  defp format_csv_datetime(nil), do: ""
  defp format_csv_datetime(dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S")

  # ── Campaign CRUD ────────────────────────────────────────────────

  def list_campaigns do
    from(c in Campaign, order_by: [desc: c.inserted_at])
    |> Repo.all()
  end

  def list_campaigns_paginated(opts \\ []) do
    from(c in Campaign, order_by: [desc: c.inserted_at])
    |> Berrypod.Pagination.paginate(page: opts[:page], per_page: opts[:per_page] || 25)
  end

  def get_campaign!(id), do: Repo.get!(Campaign, id)

  def create_campaign(attrs) do
    %Campaign{}
    |> Campaign.changeset(attrs)
    |> Repo.insert()
  end

  def update_campaign(%Campaign{} = campaign, attrs) do
    campaign
    |> Campaign.changeset(attrs)
    |> Repo.update()
  end

  def delete_campaign(%Campaign{status: "draft"} = campaign) do
    Repo.delete(campaign)
  end

  def delete_campaign(_campaign), do: {:error, :not_draft}

  @doc "Marks a campaign as sending and enqueues the send worker."
  def send_campaign_now(%Campaign{status: status} = campaign)
      when status in ["draft", "scheduled"] do
    case update_campaign(campaign, %{status: "sending"}) do
      {:ok, updated} ->
        Berrypod.Newsletter.CampaignSendWorker.enqueue(updated.id)
        {:ok, updated}

      error ->
        error
    end
  end

  def send_campaign_now(_), do: {:error, :invalid_status}

  @doc "Schedules a campaign for future sending."
  def schedule_campaign(%Campaign{status: "draft"} = campaign, %DateTime{} = at) do
    update_campaign(campaign, %{status: "scheduled", scheduled_at: at})
  end

  def schedule_campaign(_, _), do: {:error, :not_draft}

  @doc "Cancels a scheduled campaign."
  def cancel_campaign(%Campaign{status: "scheduled"} = campaign) do
    update_campaign(campaign, %{status: "cancelled", scheduled_at: nil})
  end

  def cancel_campaign(_), do: {:error, :not_scheduled}

  @doc "Returns a preview of the campaign body with a sample unsubscribe URL."
  def preview_campaign(%Campaign{body: body}) do
    sample_url = BerrypodWeb.Endpoint.url() <> "/unsubscribe/sample-token"
    String.replace(body, "{{unsubscribe_url}}", sample_url)
  end

  # ── Settings ─────────────────────────────────────────────────────

  def newsletter_enabled? do
    Settings.get_setting("newsletter_enabled", false) == true
  end

  def set_newsletter_enabled(enabled?) when is_boolean(enabled?) do
    Settings.put_setting("newsletter_enabled", enabled?, "boolean")
  end

  # ── Token helpers ────────────────────────────────────────────────

  defp generate_token do
    raw = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
    {raw, hash_token(raw)}
  end

  defp hash_token(raw) do
    :crypto.hash(:sha256, raw) |> Base.encode16(case: :lower)
  end

  # ── Confirmation email enqueue ───────────────────────────────────

  defp enqueue_confirmation(subscriber) do
    %{subscriber_id: subscriber.id, raw_token: subscriber.confirmation_token}
    |> Berrypod.Newsletter.ConfirmationEmailWorker.new()
    |> Oban.insert()
  end
end
