defmodule Mix.Tasks.Drops.Relation.RefreshCache do
  @moduledoc """
  Refreshes the Drops.Relation cache for all tables in specified repositories.

  This task clears the existing cache and warms it up again by inferring schemas
  for all tables in the database. This is useful when database schema changes
  have been made and you want to ensure the cache is up to date.

  ## Usage

      mix drops.relation.refresh_cache [options]

  ## Options

    * `--repo` - The repository to refresh cache for (can be specified multiple times)
    * `--all-repos` - Refresh cache for all configured repositories
    * `--tables` - Comma-separated list of specific tables to refresh (optional)
    * `--verbose` - Show detailed output during cache refresh

  ## Examples

      # Refresh cache for all configured repositories
      mix drops.relation.refresh_cache --all-repos

      # Refresh cache for a specific repository
      mix drops.relation.refresh_cache --repo MyApp.Repo

      # Refresh cache for specific tables only
      mix drops.relation.refresh_cache --repo MyApp.Repo --tables users,posts,comments

      # Verbose output
      mix drops.relation.refresh_cache --repo MyApp.Repo --verbose

  ## Notes

  - This task requires the application to be started to access repository configuration
  - If no repositories are specified, it will use all repositories configured in `:ecto_repos`
  - The cache must be enabled for this task to have any effect
  - Tables that don't exist in the database will be skipped with a warning
  """

  use Mix.Task

  import Mix.Ecto

  require Logger

  @shortdoc "Refreshes the Drops.Relation cache for all tables"

  @switches [
    repo: [:string, :keep],
    all_repos: :boolean,
    tables: :string,
    verbose: :boolean,
    help: :boolean
  ]

  @aliases [
    r: :repo,
    a: :all_repos,
    t: :tables,
    v: :verbose,
    h: :help
  ]

  @impl Mix.Task
  def run(args) do
    {opts, _remaining, _invalid} = OptionParser.parse(args, strict: @switches, aliases: @aliases)

    if opts[:help] do
      Mix.shell().info(@moduledoc)
      :ok
    else
      # Ensure application is started
      Mix.Task.run("app.start")

      # Get repositories to process
      repos = get_repos(opts, args)

      if Enum.empty?(repos) do
        Logger.error("No repositories found. Use --repo or --all-repos to specify repositories.")
        {:error, :no_repos}
      else
        process_cache_refresh(repos, opts)
      end
    end
  end

  defp process_cache_refresh(repos, opts) do
    tables = parse_tables(opts[:tables])
    verbose = opts[:verbose] || false

    if verbose do
      Logger.info("Refreshing Drops.Relation cache...")
      Logger.info("Repositories: #{inspect(repos)}")
      Logger.info("Tables: #{if tables, do: inspect(tables), else: "all"}")
    end

    # Process each repository
    results =
      Enum.map(repos, fn repo ->
        refresh_repo_cache(repo, tables, verbose)
      end)

    # Report results
    report_results(results, verbose)
  end

  # Private functions

  defp get_repos(opts, args) do
    cond do
      opts[:all_repos] ->
        parse_repo(args)

      opts[:repo] ->
        opts[:repo]
        |> List.wrap()
        |> Enum.map(fn repo_name ->
          ensure_repo(Module.concat([repo_name]), args)
        end)

      true ->
        parse_repo(args)
    end
  end

  defp parse_tables(nil), do: nil

  defp parse_tables(tables_string) do
    tables_string
    |> String.split(",")
    |> Enum.map(&String.trim/1)
    |> Enum.reject(&(&1 == ""))
  end

  defp refresh_repo_cache(repo, tables, verbose) do
    if verbose do
      Logger.info("Processing repository: #{inspect(repo)}")
    end

    Drops.Relation.Cache.clear_repo_cache(repo)

    if verbose do
      Logger.info("  Cache cleared for #{inspect(repo)}")
    end

    # Warm up the cache
    warm_up_result =
      if tables do
        # Warm up specific tables
        Drops.Relation.Cache.warm_up(repo, tables)
      else
        # Use the new high-level API to load cache for all tables
        case Drops.Relation.load_cache(repo) do
          :ok ->
            # Get table count for reporting
            case get_all_tables(repo) do
              [] -> {:ok, []}
              all_tables -> {:ok, all_tables}
            end

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

    case warm_up_result do
      {:ok, warmed_tables} ->
        table_count = if is_list(warmed_tables), do: length(warmed_tables), else: 0

        if verbose do
          Logger.info("  Cache warmed up for #{table_count} tables")
        end

        {:ok, repo, :refreshed, table_count}

      {:error, reason} ->
        Logger.error("  Failed to warm up cache for #{inspect(repo)}: #{inspect(reason)}")
        {:error, repo, reason}
    end
  end

  defp get_all_tables(repo) do
    case Drops.SQL.Database.list_tables(repo) do
      {:ok, tables} ->
        tables

      {:error, reason} ->
        Logger.error("  Failed to list tables from #{inspect(repo)}: #{inspect(reason)}")
        []
    end
  end

  defp report_results(results, verbose) do
    successful = Enum.count(results, &match?({:ok, _, _, _}, &1))
    failed = Enum.count(results, &match?({:error, _, _}, &1))

    # Only use Mix.shell for the final completion message that tests expect
    Mix.shell().info("Cache refresh completed")

    if verbose do
      Logger.info("Successful: #{successful}")
      Logger.info("Failed: #{failed}")
    end

    if failed > 0 do
      Logger.error("Failed to cache schemas for #{failed} repositories")

      results
      |> Enum.filter(&match?({:error, _, _}, &1))
      |> Enum.each(fn {:error, repo, reason} ->
        Logger.error("Repository #{inspect(repo)}: #{inspect(reason)}")
      end)
    end

    if failed > 0, do: {:error, :some_failed}, else: :ok
  end
end
