defmodule Mix.Tasks.Wnh.StagingE2e do
  use Mix.Task

  import Ecto.Query

  alias WhoNeedHelp.Accounts.User
  alias WhoNeedHelp.Activities.Activity
  alias WhoNeedHelp.Catalog.CategoryProposal
  alias WhoNeedHelp.Help.{Assignment, HelpRequest}
  alias WhoNeedHelp.Messaging.Message
  alias WhoNeedHelp.Repo
  alias WhoNeedHelp.Tracking.{Position, TrackingSession}
  alias WhoNeedHelp.Trust.{AbuseSignal, AuditEvent, Report, Review}

  @shortdoc "Prepares or removes an exact public-staging browser fixture"
  @confirmation "public-staging-e2e"

  @impl Mix.Task
  def run([action]) when action in ["prepare", "cleanup"] do
    Mix.Task.run("app.start")
    context = verified_context()

    case action do
      "prepare" -> prepare(context)
      "cleanup" -> cleanup(context)
    end
  end

  def run(_args) do
    Mix.raise("usage: mix wnh.staging_e2e prepare|cleanup")
  end

  defp verified_context do
    run_id = required_env!("WNH_STAGING_E2E_RUN_ID")
    expected_database = required_env!("WNH_STAGING_E2E_EXPECTED_DATABASE")
    manifest_path = required_env!("WNH_STAGING_E2E_MANIFEST_PATH") |> Path.expand()

    unless Regex.match?(~r/^[a-z0-9-]+$/, run_id) do
      Mix.raise("WNH_STAGING_E2E_RUN_ID may contain only lowercase letters, numbers, and dash")
    end

    unless System.get_env("WNH_STAGING_E2E_CONFIRM") == @confirmation do
      Mix.raise("WNH_STAGING_E2E_CONFIRM must equal #{@confirmation}")
    end

    unless String.starts_with?(manifest_path, "/output/") do
      Mix.raise("WNH_STAGING_E2E_MANIFEST_PATH must resolve below /output")
    end

    %Postgrex.Result{rows: [[actual_database]]} =
      Repo.query!("SELECT current_database()", [], log: false)

    unless actual_database == expected_database do
      Mix.raise(
        "refusing staging E2E mutation: expected database #{inspect(expected_database)}, " <>
          "observed #{inspect(actual_database)}"
      )
    end

    %{run_id: run_id, database: actual_database, manifest_path: manifest_path}
  end

  defp prepare(context) do
    password = required_env!("WNH_STAGING_E2E_PASSWORD")

    unless byte_size(password) in 12..72 do
      Mix.raise("WNH_STAGING_E2E_PASSWORD must contain between 12 and 72 bytes")
    end

    emails = emails(context.run_id)

    if Repo.exists?(from(user in User, where: user.email in ^Map.values(emails))) do
      Mix.raise("staging E2E users already exist for #{inspect(context.run_id)}")
    end

    password_hash = Bcrypt.hash_pwd_salt(password)
    now = DateTime.utc_now(:second)

    {:ok, users} =
      Repo.transaction(fn ->
        %{
          requester: insert_user!(emails.requester, "Staging E2E Requester", password_hash, now),
          helper: insert_user!(emails.helper, "Staging E2E Helper", password_hash, now)
        }
      end)

    manifest = %{
      "schema_version" => 1,
      "run_id" => context.run_id,
      "database" => context.database,
      "requester_id" => users.requester.id,
      "requester_email" => users.requester.email,
      "helper_id" => users.helper.id,
      "helper_email" => users.helper.email
    }

    context.manifest_path |> Path.dirname() |> File.mkdir_p!()
    File.write!(context.manifest_path, Jason.encode_to_iodata!(manifest, pretty: true))
    File.chmod!(context.manifest_path, 0o600)

    Mix.shell().info("prepared exact staging E2E users for #{context.run_id}")
  end

  defp cleanup(context) do
    manifest = context.manifest_path |> File.read!() |> Jason.decode!()
    validate_manifest!(context, manifest)

    requester_id = manifest["requester_id"]
    helper_id = manifest["helper_id"]
    user_ids = [requester_id, helper_id]

    request_ids =
      HelpRequest
      |> where([request], request.requester_id == ^requester_id)
      |> select([request], request.id)
      |> Repo.all()

    assignment_ids =
      Assignment
      |> where([assignment], assignment.request_id in ^request_ids)
      |> select([assignment], assignment.id)
      |> Repo.all()

    validate_owned_domain!(user_ids, request_ids, assignment_ids, helper_id)

    {:ok, deleted} =
      Repo.transaction(fn ->
        tracking_session_ids =
          TrackingSession
          |> where([session], session.assignment_id in ^assignment_ids)
          |> select([session], session.id)
          |> Repo.all()

        message_ids =
          Message
          |> where([message], message.assignment_id in ^assignment_ids)
          |> select([message], message.id)
          |> Repo.all()

        reports =
          Report
          |> where(
            [report],
            report.reporter_id in ^user_ids or report.request_id in ^request_ids or
              report.assignment_id in ^assignment_ids or report.message_id in ^message_ids
          )
          |> Repo.delete_all()
          |> elem(0)

        audits =
          AuditEvent
          |> where(
            [event],
            event.actor_id in ^user_ids or
              (event.target_type == "request" and event.target_id in ^request_ids) or
              (event.target_type == "assignment" and event.target_id in ^assignment_ids)
          )
          |> Repo.delete_all()
          |> elem(0)

        positions =
          Position
          |> where([position], position.tracking_session_id in ^tracking_session_ids)
          |> Repo.delete_all()
          |> elem(0)

        sessions =
          TrackingSession
          |> where([session], session.id in ^tracking_session_ids)
          |> Repo.delete_all()
          |> elem(0)

        messages =
          Message
          |> where([message], message.assignment_id in ^assignment_ids)
          |> Repo.delete_all()
          |> elem(0)

        abuse_signals =
          AbuseSignal
          |> where(
            [signal],
            signal.subject_id in ^user_ids or signal.assignment_id in ^assignment_ids
          )
          |> Repo.delete_all()
          |> elem(0)

        reviews =
          Review
          |> where(
            [review],
            review.assignment_id in ^assignment_ids or review.reviewer_id in ^user_ids or
              review.reviewee_id in ^user_ids
          )
          |> Repo.delete_all()
          |> elem(0)

        assignments =
          Assignment
          |> where([assignment], assignment.id in ^assignment_ids)
          |> Repo.delete_all()
          |> elem(0)

        requests =
          HelpRequest
          |> where([request], request.id in ^request_ids)
          |> Repo.delete_all()
          |> elem(0)

        users =
          User
          |> where([user], user.id in ^user_ids)
          |> Repo.delete_all()
          |> elem(0)

        %{
          reports: reports,
          audit_events: audits,
          tracking_positions: positions,
          tracking_sessions: sessions,
          messages: messages,
          abuse_signals: abuse_signals,
          reviews: reviews,
          assignments: assignments,
          requests: requests,
          users: users
        }
      end)

    unless deleted.users == 2 do
      Mix.raise("cleanup removed #{deleted.users} users instead of exactly 2")
    end

    Mix.shell().info("removed exact staging E2E fixture: #{inspect(deleted)}")
  end

  defp validate_manifest!(context, manifest) do
    expected_emails = emails(context.run_id)

    unless manifest["schema_version"] == 1 and manifest["run_id"] == context.run_id and
             manifest["database"] == context.database and
             manifest["requester_email"] == expected_emails.requester and
             manifest["helper_email"] == expected_emails.helper and
             uuid?(manifest["requester_id"]) and uuid?(manifest["helper_id"]) do
      Mix.raise("staging E2E manifest does not match the requested run and database")
    end

    expected =
      MapSet.new([
        {manifest["requester_id"], manifest["requester_email"]},
        {manifest["helper_id"], manifest["helper_email"]}
      ])

    observed =
      User
      |> where([user], user.id in ^[manifest["requester_id"], manifest["helper_id"]])
      |> select([user], {user.id, user.email})
      |> Repo.all()
      |> MapSet.new()

    unless observed == expected do
      Mix.raise("staging E2E user ownership does not match the manifest")
    end
  end

  defp validate_owned_domain!(user_ids, request_ids, assignment_ids, helper_id) do
    unexpected_request? =
      Repo.exists?(
        from(request in HelpRequest,
          where: request.requester_id in ^user_ids and request.id not in ^request_ids
        )
      )

    unexpected_assignment? =
      Repo.exists?(
        from(assignment in Assignment,
          where:
            assignment.helper_id in ^user_ids and
              (assignment.id not in ^assignment_ids or assignment.helper_id != ^helper_id)
        )
      )

    unexpected_activity? =
      Repo.exists?(from(activity in Activity, where: activity.creator_id in ^user_ids))

    unexpected_proposal? =
      Repo.exists?(from(proposal in CategoryProposal, where: proposal.proposer_id in ^user_ids))

    unless length(request_ids) <= 2 and length(assignment_ids) <= 2 and
             not unexpected_request? and not unexpected_assignment? and
             not unexpected_activity? and not unexpected_proposal? do
      Mix.raise("staging E2E users own records outside the exact medicine-flow scope")
    end
  end

  defp insert_user!(email, display_name, password_hash, now) do
    %User{}
    |> User.registration_changeset(%{
      "email" => email,
      "display_name" => display_name,
      "locale" => "en",
      "terms_accepted" => true
    })
    |> Ecto.Changeset.put_change(:hashed_password, password_hash)
    |> Ecto.Changeset.put_change(:confirmed_at, now)
    |> Repo.insert!()
  end

  defp emails(run_id) do
    %{
      requester: "wnh-staging-e2e-#{run_id}-requester@example.invalid",
      helper: "wnh-staging-e2e-#{run_id}-helper@example.invalid"
    }
  end

  defp required_env!(name) do
    case System.get_env(name) do
      value when is_binary(value) and value != "" -> value
      _ -> Mix.raise("#{name} is required")
    end
  end

  defp uuid?(value) when is_binary(value) do
    match?({:ok, _}, Ecto.UUID.cast(value))
  end

  defp uuid?(_value), do: false
end
