defmodule Mix.Tasks.Workspace.List do
  opts = [
    format: [
      type: :string,
      default: "pretty",
      doc: """
      The output format of the list. It can be one of the following:
        * `json` - pretty prints the list as a json.
        * `pretty` - pretty prints the list.
      """,
      allowed: ["json", "pretty"]
    ],
    json: [
      type: :boolean,
      doc: """
      If set a `json` file will be generated with the list of workspace projects and
      associated metadata. By default it will be saved in `workspace.json` in the
      current directory. You can override the output path by setting the `--output`
      option.
      """,
      deprecated: "Use `--format json` with `--output` instead.",
      doc_section: :export
    ],
    output: [
      type: :string,
      doc: """
      Save the list to a file. Applicable only if `--format` is set to `json`.
      """,
      doc_section: :export
    ],
    relative_paths: [
      type: :boolean,
      default: false,
      doc: """
      If set the paths in the exported json file will be relative with respect to the
      workspace path. Applicable only if `--format` is set to `json`.
      """,
      doc_section: :export
    ],
    maintainer: [
      type: :string,
      doc: """
      Search for projects with the given maintainer. A partial case insensitive string search
      is performed so you can provide only part of the maintainer's name.
      """,
      doc_section: :filtering
    ]
  ]

  @options_schema Workspace.Cli.options(
                    [
                      :workspace_path,
                      :config_path,
                      :project,
                      :exclude,
                      :paths,
                      :tags,
                      :excluded_tags,
                      :dependency,
                      :dependent,
                      :show_status,
                      :affected,
                      :modified,
                      :base,
                      :head
                    ],
                    opts
                  )

  @shortdoc "Display info about the workspace projects"

  @moduledoc """
  Shows workspace project info

      $ mix workspace.list

  By default the following are displayed:

  - the project app name
  - the project path with respect to workspace path
  - the description if set
  - the project tags

  ## Command line options

  #{CliOptions.docs(@options_schema, sort: true, sections: Workspace.CliOptions.doc_sections())}

  ## Filtering projects

  Several command line options can limit the returned projects and filter the
  workspace.

  You can list only projects with a specific tag:

      $ mix workspace.list --tag core

  Or exclude projects with a specific tag:

      $ mix workspace.list --exclude-tag deprecated

  If your monorepo has a nested structure you can list projects only under one or more
  specific paths.

      $ mix workspace.list --path packages/shared --path packages/infra

  In large monorepos you may want to get all projects depending on a specific package. You
  can achieve this through the `--dependency` option:

      $ mix workspace.list --dependency foo

  Additionally you can specify the `--dependent` flag to consider only dependencies of a
  given project:

      $ mix workspace.list --dependent foo

  You can also filter by the project's maintainer. The search is case insensitive. The
  maintainers are expected to be defined under `package`:

      def project do
        [
          package: [
            maintainers: ["Jack Sparrow"]
          ],
          # rest project options
        ]

  In order to get all projects associated with a specific maintainer:

      $ mix workspace.list --maintainer "Jack Sparrow"

      # notice that the search is case insensitive, this works as well
      $ mix workspace.list --maintainer sparrow

  """
  use Mix.Task
  alias Workspace.Cli

  @impl Mix.Task
  def run(args) do
    {opts, _args, _extra} = CliOptions.parse!(args, @options_schema)

    opts
    |> Mix.WorkspaceUtils.load_and_filter_workspace()
    |> maybe_search_projects(opts)
    |> list_or_save_workspace_projects(opts)
  end

  defp maybe_search_projects(workspace, opts) do
    search_by_maintainer(workspace, opts[:maintainer])
  end

  defp search_by_maintainer(workspace, nil), do: workspace

  defp search_by_maintainer(workspace, maintainer) do
    projects =
      Enum.map(workspace.projects, fn {_name, project} ->
        case matches_maintainer?(project, maintainer) do
          true -> project
          false -> Map.put(project, :skip, true)
        end
      end)

    Workspace.State.set_projects(workspace, projects)
  end

  defp matches_maintainer?(project, maintainer) do
    maintainers = get_in(project.config, [:package, :maintainers]) || []
    maintainers = maintainers |> Enum.join(" ") |> String.downcase()

    String.contains?(maintainers, String.downcase(maintainer))
  end

  defp list_or_save_workspace_projects(workspace, opts) do
    # TODO: remove --json support in 0.4.0
    opts =
      if opts[:json] do
        Keyword.merge(opts, format: "json", output: opts[:output] || "workspace.json")
      else
        opts
      end

    output_result(workspace, opts, opts[:format])
  end

  defp output_result(workspace, opts, "pretty") do
    projects = Workspace.projects(workspace)

    case Enum.count(projects, &(not &1.skip)) do
      0 ->
        Cli.log([:bright, :yellow, "No matching projects for the given options", :reset])

      valid ->
        Cli.log([
          "Found ",
          :bright,
          :blue,
          "#{valid} workspace projects",
          :reset,
          " matching the given options."
        ])

        max_project_length = max_project_length(projects)

        projects
        |> Enum.sort_by(& &1.app)
        |> Enum.each(&print_project_info(&1, max_project_length, opts[:show_status]))
    end
  end

  defp output_result(workspace, opts, "json") do
    json_data = Workspace.Export.to_json(workspace, sort: true, relative: opts[:relative_paths])

    case opts[:output] do
      nil ->
        IO.puts(json_data)

      output ->
        File.write!(output, json_data)
        Workspace.Cli.log([:green, "generated ", :reset, output], prefix: :header)
    end
  end

  defp max_project_length(projects) do
    projects
    |> Enum.map(fn project -> inspect(project.app) |> String.length() end)
    |> Enum.max()
  end

  defp print_project_info(%Workspace.Project{skip: true}, _length, _show_status), do: :ok

  defp print_project_info(project, max_project_length, show_status) do
    indent_size = max_project_length - String.length(inspect(project.app))
    indent = String.duplicate(" ", indent_size)

    Cli.log([
      "  * ",
      Cli.project_name(project, show_status: show_status, pretty: true),
      indent,
      :light_black,
      " ",
      Path.relative_to(project.mix_path, project.workspace_path),
      :reset,
      description(project.config[:description]),
      tags(project.tags)
    ])
  end

  defp description(nil), do: ""
  defp description(doc) when is_binary(doc), do: [" - ", doc]

  defp tags([]), do: []

  defp tags(tags) do
    tags =
      Enum.map(tags, fn tag -> [:tag, Workspace.Project.format_tag(tag), :reset] end)
      |> Enum.intersperse(", ")

    [" " | tags]
  end
end
