defmodule Oli.Authoring.Course do
  import Ecto.Query, warn: false

  alias Oli.Accounts.Author
  alias Oli.Authoring.Authors.{AuthorProject, ProjectRole}
  alias Oli.Authoring.{Collaborators, ProjectSearch}

  alias Oli.Authoring.Course.{
    Project,
    Family,
    ProjectResource,
    ProjectAttributes,
    ProjectVisibility
  }

  alias Oli.Groups.CommunityVisibility
  alias Oli.Inventories
  alias Oli.Publishing
  alias Oli.Publishing.Publications.Publication
  alias Oli.Publishing.PublishedResource
  alias Oli.{Accounts, Repo}
  alias Oli.Repo.{Paging, Sorting}
  alias Oli.Resources.{ResourceType, Revision, ScoringStrategy}
  alias Oli.Delivery.Sections.SectionsProjectsPublications
  alias Oli.Publishing.PublishedResource

  def create_project_resource(attrs) do
    %ProjectResource{}
    |> ProjectResource.changeset(attrs)
    |> Repo.insert()
  end

  def list_project_resources(project_id) do
    Repo.all(
      from(pr in ProjectResource,
        where: pr.project_id == ^project_id,
        select: pr
      )
    )
  end

  def change_project_resource(%ProjectResource{} = project_resource, attrs \\ %{}) do
    ProjectResource.changeset(project_resource, attrs)
  end

  def list_projects do
    Repo.all(Project)
  end

  @doc """
  Lists all projects that contain a particular resource.
  """
  def list_projects_containing_resource(resource_id) do
    Repo.all(
      from(pr in ProjectResource,
        join: p in Project,
        on: p.id == pr.project_id,
        where: pr.resource_id == ^resource_id,
        select: p
      )
    )
  end

  @doc """
  Get all the projects that are not associated within a community.

  ## Examples

      iex> list_projects_not_in_community(1)
      {:ok, [%Project{}, ,...]}

      iex> list_projects_not_in_community(123)
      {:ok, []}
  """
  def list_projects_not_in_community(community_id) do
    from(
      project in Project,
      left_join: community_visibility in CommunityVisibility,
      on:
        project.id ==
          community_visibility.project_id and community_visibility.community_id == ^community_id,
      where: is_nil(community_visibility.id) and project.status == :active,
      select: project
    )
    |> Repo.all()
  end

  def get_projects_for_author(author) do
    if Accounts.has_admin_role?(author, :content_admin),
      do: Repo.all(Project),
      else: Repo.preload(author, [:projects]).projects
  end

  def browse_projects(
        %Author{} = author,
        %Paging{} = paging,
        %Sorting{} = sorting,
        opts \\ []
      ) do
    include_deleted = Keyword.get(opts, :include_deleted, false)
    admin_show_all = Keyword.get(opts, :admin_show_all, true)
    text_search = Keyword.get(opts, :text_search, "")

    if Accounts.has_admin_role?(author, :content_admin) and admin_show_all,
      do: browse_projects_as_admin(paging, sorting, include_deleted, text_search),
      else: browse_projects_as_author(author, paging, sorting, include_deleted, text_search)
  end

  defp browse_projects_as_admin(
         %Paging{limit: limit, offset: offset},
         %Sorting{direction: direction, field: field},
         include_deleted,
         text_search
       ) do
    filter_by_status =
      if include_deleted, do: dynamic([p], true), else: dynamic([p], p.status == :active)

    filter_by_text =
      text_search
      |> project_search_patterns()
      |> Enum.reduce(dynamic([p], true), fn pattern, acc ->
        dynamic(
          [p, ap, a, _, _, _, owner],
          ^acc and
            (ilike(p.title, ^pattern) or ilike(p.slug, ^pattern) or ilike(owner.name, ^pattern) or
               ilike(owner.email, ^pattern) or ilike(a.name, ^pattern))
        )
      end)

    where_clause = dynamic([p], ^filter_by_status and ^filter_by_text)

    owner_id = Oli.Authoring.Authors.ProjectRole.role_id().owner

    query =
      from p in Project,
        join: ap in AuthorProject,
        on: ap.project_id == p.id,
        join: a in Author,
        on: ap.author_id == a.id,
        left_join: pv in ProjectVisibility,
        on: pv.project_id == p.id,
        left_join: pub in Publication,
        on: pub.project_id == p.id and not is_nil(pub.published),
        left_join: op in AuthorProject,
        on: op.project_id == p.id and op.project_role_id == ^owner_id,
        left_join: owner in Author,
        on: owner.id == op.author_id,
        where: ^where_clause,
        group_by: [
          p.id,
          p.slug,
          p.title,
          p.inserted_at,
          p.status,
          p.visibility,
          owner.id,
          owner.name,
          owner.email
        ],
        limit: ^limit,
        offset: ^offset,
        select: %{
          id: p.id,
          slug: p.slug,
          title: p.title,
          inserted_at: p.inserted_at,
          status: p.status,
          visibility: p.visibility,
          owner_id: owner.id,
          name: owner.name,
          email: owner.email,
          total_count: fragment("count(*) OVER()"),
          collaborators:
            fragment(
              "json_agg(DISTINCT jsonb_build_object('id', ?, 'name', ?)) FILTER (WHERE ? IS NOT NULL)",
              a.id,
              a.name,
              a.id
            ),
          published: fragment("bool_or(?)", not is_nil(pub.id))
        }

    query =
      case field do
        :name ->
          order_by(query, [_, _, _, _, _, _, owner], {^direction, owner.name})

        :collaborators ->
          order_by(
            query,
            [_, _, a],
            {^direction, fragment("string_agg(DISTINCT ?, ', ')", a.name)}
          )

        :published ->
          order_by(
            query,
            [_, _, _, _, pub],
            {^direction, fragment("bool_or(?)", not is_nil(pub.id))}
          )

        :visibility ->
          order_by(query, [p], {
            ^direction,
            fragment(
              "CASE ? WHEN 'global' THEN 0 WHEN 'selected' THEN 1 WHEN 'authors' THEN 2 ELSE 3 END",
              p.visibility
            )
          })

        _ ->
          order_by(query, [p, _, _, _, _, _, _, _], {^direction, field(p, ^field)})
      end

    Repo.all(query)
  end

  defp browse_projects_as_author(
         %Author{id: id},
         %Paging{limit: limit, offset: offset},
         %Sorting{direction: direction, field: field},
         include_deleted,
         text_search
       ) do
    owner_id = Oli.Authoring.Authors.ProjectRole.role_id().owner

    filter_by_status =
      if include_deleted, do: dynamic([_, p], true), else: dynamic([_, p], p.status == :active)

    filter_by_text =
      text_search
      |> project_search_patterns()
      |> Enum.reduce(dynamic([_, p], true), fn pattern, acc ->
        dynamic(
          [_, p, a, _, _, _, owner],
          ^acc and
            (ilike(p.title, ^pattern) or ilike(p.slug, ^pattern) or ilike(owner.name, ^pattern) or
               ilike(owner.email, ^pattern) or ilike(a.name, ^pattern))
        )
      end)

    where_clause =
      dynamic(
        [ap, p],
        ap.author_id == ^id and ap.status == :accepted and ^filter_by_status and ^filter_by_text
      )

    query =
      from ap in AuthorProject,
        join: p in Project,
        on: ap.project_id == p.id,
        join: a in Author,
        on: ap.author_id == a.id,
        left_join: pv in ProjectVisibility,
        on: pv.project_id == p.id,
        left_join: pub in Publication,
        on: pub.project_id == p.id and not is_nil(pub.published),
        left_join: op in AuthorProject,
        on: op.project_id == p.id and op.project_role_id == ^owner_id,
        left_join: owner in Author,
        on: owner.id == op.author_id,
        where: ^where_clause,
        group_by: [
          p.id,
          p.slug,
          p.title,
          p.inserted_at,
          p.status,
          p.visibility,
          owner.id,
          owner.name,
          owner.email
        ],
        limit: ^limit,
        offset: ^offset,
        select: %{
          id: p.id,
          slug: p.slug,
          title: p.title,
          inserted_at: p.inserted_at,
          status: p.status,
          visibility: p.visibility,
          owner_id: owner.id,
          name: owner.name,
          email: owner.email,
          total_count: fragment("count(*) OVER()"),
          collaborators:
            fragment(
              "json_agg(DISTINCT jsonb_build_object('id', ?, 'name', ?)) FILTER (WHERE ? IS NOT NULL)",
              a.id,
              a.name,
              a.id
            ),
          published: fragment("bool_or(?)", not is_nil(pub.id))
        }

    query =
      case field do
        :name ->
          order_by(query, [_, _, _, _, _, _, _, owner], {^direction, owner.name})

        :collaborators ->
          order_by(
            query,
            [_, _, a],
            {^direction, fragment("string_agg(DISTINCT ?, ', ')", a.name)}
          )

        :published ->
          order_by(
            query,
            [_, _, _, _, _, pub],
            {^direction, fragment("bool_or(?)", not is_nil(pub.id))}
          )

        :visibility ->
          order_by(query, [_, p], {
            ^direction,
            fragment(
              "CASE ? WHEN 'global' THEN 0 WHEN 'selected' THEN 1 WHEN 'authors' THEN 2 ELSE 3 END",
              p.visibility
            )
          })

        _ ->
          order_by(query, [_, p, _, _, _, _, _, _], {^direction, field(p, ^field)})
      end

    Repo.all(query)
  end

  @doc """
  Browse projects for CSV export without pagination limits.

  Returns all projects that match the filtering criteria without pagination.
  This function reuses the same filtering and sorting logic as browse_projects/4
  but removes pagination limits for export purposes.

  ## Parameters
    - author: The current author requesting the export
    - sorting: Sorting parameters (field and direction)
    - opts: Options including:
      - include_deleted: Include deleted projects (default: false)
      - admin_show_all: Admin view shows all projects (default: true for admins)
      - text_search: Text search filter (default: "")

  ## Examples
      iex> browse_projects_for_export(author, %Sorting{}, [])
      [%{id: 1, title: "Project 1", ...}, ...]
  """
  def browse_projects_for_export(
        %Author{} = author,
        %Sorting{} = sorting,
        opts \\ []
      ) do
    include_deleted = Keyword.get(opts, :include_deleted, false)
    admin_show_all = Keyword.get(opts, :admin_show_all, true)
    text_search = Keyword.get(opts, :text_search, "")

    if Accounts.has_admin_role?(author, :content_admin) and admin_show_all,
      do: browse_projects_as_admin_for_export(sorting, include_deleted, text_search),
      else: browse_projects_as_author_for_export(author, sorting, include_deleted, text_search)
  end

  defp browse_projects_as_admin_for_export(
         %Sorting{direction: direction, field: field},
         include_deleted,
         text_search
       ) do
    filter_by_status = if include_deleted, do: true, else: dynamic([p], p.status == :active)

    filter_by_text =
      if text_search == "", do: true, else: dynamic([p], ilike(p.title, ^"%#{text_search}%"))

    owner_id = ProjectRole.role_id().owner

    query =
      Project
      |> join(:left, [p], ap in AuthorProject,
        on: ap.project_id == p.id and ap.status == ^:accepted
      )
      |> join(:left, [p, ap], a in Author, on: ap.author_id == a.id)
      |> join(:left, [p, ap, a], op in AuthorProject,
        on: op.project_id == p.id and op.project_role_id == ^owner_id
      )
      |> join(:left, [p, ap, a, op], owner in Author, on: owner.id == op.author_id)
      |> where(^filter_by_status)
      |> where(^filter_by_text)
      |> group_by([p, ap, a, op, owner], [
        p.id,
        p.slug,
        p.title,
        p.inserted_at,
        p.status,
        p.visibility,
        owner.id,
        owner.name,
        owner.email
      ])
      |> select([p, ap, a, op, owner], %{
        id: p.id,
        slug: p.slug,
        title: p.title,
        inserted_at: p.inserted_at,
        status: p.status,
        visibility: p.visibility,
        published:
          fragment(
            "EXISTS (SELECT 1 FROM publications pub WHERE pub.project_id = ? AND pub.published IS NOT NULL)",
            p.id
          ),
        owner_id: owner.id,
        name: owner.name,
        email: owner.email,
        collaborators:
          fragment(
            "json_agg(DISTINCT jsonb_build_object('id', ?, 'name', ?)) FILTER (WHERE ? IS NOT NULL)",
            a.id,
            a.name,
            a.id
          )
      })

    query =
      case field do
        :name ->
          order_by(query, [_, _, _, _, owner], {^direction, owner.name})

        :collaborators ->
          order_by(query, [_, _, a, _, _], {
            ^direction,
            fragment("string_agg(DISTINCT ?, ', ')", a.name)
          })

        :published ->
          order_by(query, [p, _, _, _, _], {
            ^direction,
            fragment(
              "EXISTS (SELECT 1 FROM publications pub WHERE pub.project_id = ? AND pub.published IS NOT NULL)",
              p.id
            )
          })

        :visibility ->
          order_by(query, [p, _, _, _, _], {
            ^direction,
            fragment(
              "CASE ? WHEN 'global' THEN 0 WHEN 'selected' THEN 1 WHEN 'authors' THEN 2 ELSE 3 END",
              p.visibility
            )
          })

        _ ->
          order_by(query, [p, _, _, _, _], {^direction, field(p, ^field)})
      end

    Repo.all(query)
  end

  defp browse_projects_as_author_for_export(
         %Author{id: id},
         %Sorting{direction: direction, field: field},
         include_deleted,
         text_search
       ) do
    owner_id = ProjectRole.role_id().owner

    filter_by_collaborator =
      dynamic([_, ap, _, _, _, _], ap.author_id == ^id and ap.status == ^:accepted)

    filter_by_status =
      if include_deleted, do: true, else: dynamic([p, _, _, _, _, _], p.status == ^:active)

    filter_by_text =
      if text_search == "",
        do: true,
        else: dynamic([p, _, _, _, _, _], ilike(p.title, ^"%#{text_search}%"))

    query =
      Project
      |> join(:left, [p], ap in AuthorProject,
        on: ap.project_id == p.id and ap.status == ^:accepted
      )
      |> join(:left, [p, ap], a in Author, on: ap.author_id == a.id)
      |> join(:left, [p, ap, a], op in AuthorProject,
        on: op.project_id == p.id and op.project_role_id == ^owner_id
      )
      |> join(:left, [p, ap, a, op], owner in Author, on: owner.id == op.author_id)
      |> where(^filter_by_collaborator)
      |> where(^filter_by_status)
      |> where(^filter_by_text)
      |> group_by([p, ap, a, op, owner], [
        p.id,
        p.slug,
        p.title,
        p.inserted_at,
        p.status,
        p.visibility,
        owner.id,
        owner.name,
        owner.email
      ])
      |> select([p, ap, a, op, owner], %{
        id: p.id,
        slug: p.slug,
        title: p.title,
        inserted_at: p.inserted_at,
        status: p.status,
        visibility: p.visibility,
        published:
          fragment(
            "EXISTS (SELECT 1 FROM publications pub WHERE pub.project_id = ? AND pub.published IS NOT NULL)",
            p.id
          ),
        owner_id: owner.id,
        name: owner.name,
        email: owner.email,
        collaborators:
          fragment(
            "json_agg(DISTINCT jsonb_build_object('id', ?, 'name', ?)) FILTER (WHERE ? IS NOT NULL)",
            a.id,
            a.name,
            a.id
          )
      })

    query =
      case field do
        :name ->
          order_by(query, [_, _, _, _, owner], {^direction, owner.name})

        :collaborators ->
          order_by(query, [_, _, a, _, _], {
            ^direction,
            fragment("string_agg(DISTINCT ?, ', ')", a.name)
          })

        :published ->
          order_by(query, [p, _, _, _, _], {
            ^direction,
            fragment(
              "EXISTS (SELECT 1 FROM publications pub WHERE pub.project_id = ? AND pub.published IS NOT NULL)",
              p.id
            )
          })

        :visibility ->
          order_by(query, [p, _, _, _, _], {
            ^direction,
            fragment(
              "CASE ? WHEN 'global' THEN 0 WHEN 'selected' THEN 1 WHEN 'authors' THEN 2 ELSE 3 END",
              p.visibility
            )
          })

        _ ->
          order_by(query, [p, _, _, _, _], {^direction, field(p, ^field)})
      end

    Repo.all(query)
  end

  @spec search_published_projects(binary) :: any
  @doc """
  Returns the list of published projects where the title, description and slug are similar to the query string
  ## Examples
      iex> search_published_projects()
      [%Project{}, ...]
  """
  def search_published_projects(search_term) do
    ProjectSearch.search(search_term)
  end

  def get_project!(id), do: Repo.get!(Project, id)
  def get_project_by_slug(nil), do: nil
  def get_project_by_slug(slug) when is_binary(slug), do: Repo.get_by(Project, slug: slug)

  def get_project_attributes(nil), do: %ProjectAttributes{}

  def get_project_attributes(project_slug) when is_binary(project_slug) do
    project = get_project_by_slug(project_slug)

    case project do
      nil -> %ProjectAttributes{}
      %Project{:attributes => nil} -> %ProjectAttributes{}
      %Project{:attributes => attributes} -> attributes
    end
  end

  def create_and_attach_resource(project, attrs) do
    with {:ok, %{resource: resource, revision: revision}} <-
           Oli.Resources.create_resource_and_revision(attrs),
         {:ok, project_resource} = attach_to_project(resource, project) do
      {:ok, %{resource: resource, revision: revision, project_resource: project_resource}}
    else
      error -> error
    end
  end

  def attach_to_project(%{resource: resource}, project) do
    attach_to_project(resource, project)
  end

  def attach_to_project(resource, project) do
    create_project_resource(%{project_id: project.id, resource_id: resource.id})
  end

  def initial_resource_setup(author, project) do
    attrs = %{
      title: "Curriculum",
      author_id: author.id,
      resource_type_id: Oli.Resources.ResourceType.id_for_container()
    }

    create_and_attach_resource(project, attrs)
  end

  def create_project(attrs) do
    %Project{}
    |> Project.changeset(attrs)
    |> Repo.insert()
  end

  def create_project(title, author, additional_attrs \\ %{}) do
    Repo.transaction(fn ->
      with {:ok, project_family} <- create_family(default_family(title)),
           {:ok, project} <-
             create_project(default_project(title, project_family, additional_attrs)),
           {:ok, collaborator} <- Collaborators.add_collaborator(author, project),
           {:ok, %{resource: resource, revision: resource_revision}} <-
             initial_resource_setup(author, project),
           {:ok, %{publication: publication, published_resource: published_resource}} <-
             Publishing.initial_publication_setup(project, resource, resource_revision) do
        %{
          project_family: project_family,
          project: project,
          author_project: collaborator,
          resource: resource,
          resource_revision: resource_revision,
          publication: publication,
          published_resource: published_resource
        }
      else
        {:error, error} -> Repo.rollback(error)
      end
    end)
  end

  defp default_project(title, family, additional_attrs) do
    default_publisher = Inventories.default_publisher()

    Map.merge(
      %{
        title: title,
        version: "1.0.0",
        family_id: family.id,
        publisher_id: default_publisher.id,
        analytics_version: :v2
      },
      additional_attrs
    )
  end

  def update_project(%Project{} = project, attrs) do
    project
    |> Project.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Updates the latest_analytics_snapshot_url and latest_analytics_snapshot_timestamp for the given project.
  """
  def update_project_latest_analytics_snapshot_url(project_slug, url, timestamp) do
    get_project_by_slug(project_slug)
    |> Project.changeset(%{
      latest_analytics_snapshot_url: url,
      latest_analytics_snapshot_timestamp: timestamp
    })
    |> Repo.update()
  end

  @doc """
  Updates the latest_datashop_snapshot_url and latest_datashop_snapshot_timestamp for the given project.
  """
  def update_project_latest_datashop_snapshot_url(project_slug, url, timestamp) do
    get_project_by_slug(project_slug)
    |> Project.changeset(%{
      latest_datashop_snapshot_url: url,
      latest_datashop_snapshot_timestamp: timestamp
    })
    |> Repo.update()
  end

  @doc """
  Updates the latest_export_snapshot_url and latest_export_snapshot_timestamp for the given project.
  """
  def update_project_latest_export_url(project_slug, url, timestamp) do
    get_project_by_slug(project_slug)
    |> Project.changeset(%{
      latest_export_url: url,
      latest_export_timestamp: timestamp
    })
    |> Repo.update()
  end

  def get_family!(id), do: Repo.get!(Family, id)

  def update_family(%Family{} = family, attrs) do
    family
    |> Family.changeset(attrs)
    |> Repo.update()
  end

  def create_family(attrs \\ %{}) do
    %Family{}
    |> Family.changeset(attrs)
    |> Repo.insert()
  end

  defp default_family(title) do
    %{
      title: title,
      description: "New family from project creation"
    }
  end

  defp project_has_survey?(project_id) do
    Project
    |> where([p], p.id == ^project_id and not is_nil(p.required_survey_resource_id))
    |> select([p], p.required_survey_resource_id)
    |> Repo.one()
    |> Kernel.!=(nil)
  end

  def get_project_survey(project_id) do
    PublishedResource
    |> join(:inner, [pr], rev in Revision, on: pr.revision_id == rev.id)
    |> join(:inner, [_, _, proj], proj in Project, on: proj.id == ^project_id)
    |> where(
      [pr, rev, proj],
      pr.publication_id in subquery(
        Publication
        |> where([p], is_nil(p.published) and p.project_id == ^project_id)
        |> select([p], p.id)
      ) and pr.resource_id == proj.required_survey_resource_id
    )
    |> select([_, rev], rev)
    |> Repo.one()
  end

  defp update_project_required_survey_resource_id(project_id, resource_id) do
    Project
    |> where([p], p.id == ^project_id)
    |> Repo.one()
    |> Project.changeset(%{required_survey_resource_id: resource_id})
    |> Repo.update()
  end

  def create_project_survey(project, author_id) do
    case project_has_survey?(project.id) do
      false -> do_create_project_survey(project, author_id)
      _ -> {:error, "The project already has a survey"}
    end
  end

  defp do_create_project_survey(project, author_id) do
    {:ok, %{revision: revision}} =
      create_and_attach_resource(project, %{
        title: "Course Survey",
        author_id: author_id,
        max_attempts: 1,
        scoring_strategy_id: ScoringStrategy.get_id_by_type("most_recent"),
        resource_type_id: ResourceType.id_for_page(),
        content: %{
          "version" => "0.1.0",
          "model" => []
        }
      })

    update_project_required_survey_resource_id(project.id, revision.resource_id)

    Oli.Publishing.ChangeTracker.track_revision(project.slug, revision)

    revision
  end

  def delete_project_survey(project) do
    case project_has_survey?(project.id) do
      false -> {:error, "The project doesn't have a survey"}
      _ -> do_delete_project_survey(project)
    end
  end

  defp do_delete_project_survey(project) do
    case get_project_survey(project.id) do
      revision ->
        update_project_required_survey_resource_id(project.id, nil)
        Oli.Publishing.ChangeTracker.track_revision(project.slug, revision, %{deleted: true})
    end
  end

  @doc """
  Returns true if an export is already in progress for the given project and queue, false otherwise.

  ## Examples
      iex> export_in_progress?("example_project")
      true
  """
  def export_in_progress?(project_slug, queue) do
    Oban.Job
    |> where([j], j.state in ["available", "executing", "scheduled"])
    |> where([j], j.queue == ^queue)
    |> where([j], fragment("?->>'project_slug' = ?", j.args, ^project_slug))
    |> Repo.all()
    |> Enum.count() > 0
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking project changes.
  ## Examples
      iex> change_project(project, params)
      %Ecto.Changeset{source: %Project{}}
  """
  def change_project(project, params \\ %{}) do
    Project.changeset(project, params)
  end

  alias Oli.Authoring.Course.CreativeCommons

  @cc_options Map.keys(CreativeCommons.cc_options())

  @type license_types ::
          :none | :cc_by | :cc_by_sa | :cc_by_nd | :cc_by_nc | :cc_by_nc_sa | :cc_by_nc_nd
  @spec get_project_license(integer(), String.t()) ::
          %{license_type: :custom, custom_license_details: String.t()}
          | %{license_type: license_types, custom_license_details: <<>>}
          | nil
  def get_project_license(revision_id, section_slug) do
    from(pr in PublishedResource,
      join: spp in SectionsProjectsPublications,
      on: pr.publication_id == spp.publication_id,
      join: p in assoc(spp, :project),
      join: s in assoc(spp, :section),
      where: pr.revision_id == ^revision_id and s.slug == ^section_slug,
      distinct: true,
      select: p.attributes
    )
    |> Repo.one()
    |> case do
      %{license: %{license_type: license_type} = license} when license_type in @cc_options ->
        Map.from_struct(license)

      %{license: nil} ->
        nil

      nil ->
        nil
    end
  end

  @doc """
  Returns the author_project for a given project and author.
  The `filter_by_status` boolean option is used to filter the author_project by status
  (author_project status = :accepted and section status = :active).
  """
  def get_author_project(project_slug, author_id, opts \\ [filter_by_status: true]) do
    maybe_filter_by_status =
      if opts[:filter_by_status] do
        dynamic([ap, p], ap.status == :accepted and p.status == :active)
      else
        true
      end

    query =
      from(
        ap in AuthorProject,
        join: p in Project,
        on: ap.project_id == p.id,
        where: ap.author_id == ^author_id and p.slug == ^project_slug,
        where: ^maybe_filter_by_status,
        select: ap
      )

    Repo.one(query)
  end

  @doc """
  Updates the author_project.
  """
  def update_author_project(%AuthorProject{} = ap, attrs) do
    ap
    |> AuthorProject.changeset(attrs)
    |> Repo.update()
  end

  defp project_search_patterns(nil), do: []

  defp project_search_patterns(text_search) do
    text_search
    |> String.trim()
    |> case do
      "" ->
        []

      trimmed ->
        trimmed
        |> String.split(~r/\s+/, trim: true)
        |> Enum.map(&"%#{&1}%")
    end
  end
end
