defmodule BDS.Projects do
  @moduledoc false

  import Ecto.Query

  alias BDS.Metadata
  alias BDS.Persistence
  alias BDS.Projects.Project
  alias BDS.Repo
  alias BDS.Slug
  alias BDS.Templates

  @default_project_id "default"
  @default_project_name "My Blog"

  def list_projects do
    Repo.all(from project in Project, order_by: [asc: project.created_at])
  end

  def get_active_project do
    Repo.one(from project in Project, where: project.is_active == true, limit: 1)
  end

  def shell_snapshot do
    _ = ensure_default_project()
    projects = list_projects()
    active_project = Enum.find(projects, & &1.is_active)

    %{
      active_project_id: active_project && active_project.id,
      projects: Enum.map(projects, &project_summary/1)
    }
  end

  def get_project(id), do: Repo.get(Project, id)
  def get_project!(id), do: Repo.get!(Project, id)

  def ensure_default_project do
    case Repo.get(Project, @default_project_id) do
      %Project{} = project ->
        {:ok, project}

      nil ->
        now = Persistence.now_ms()
        is_active = not Repo.exists?(from project in Project, where: project.is_active == true)

        Repo.transaction(fn ->
          project =
            %Project{}
            |> Project.changeset(%{
              id: @default_project_id,
              name: @default_project_name,
              slug: unique_slug(Slug.slugify(@default_project_name)),
              description: nil,
              data_path: nil,
              created_at: now,
              updated_at: now,
              is_active: is_active
            })
            |> Repo.insert!()

          {:ok, _templates} = Templates.rebuild_templates_from_files(project.id)
          project
        end)
        |> case do
          {:ok, project} -> {:ok, project}
          {:error, reason} -> {:error, reason}
        end
    end
  end

  def project_data_dir(%Project{} = project) do
    project.data_path || Path.expand("../../priv/data/projects/#{project.id}", __DIR__)
  end

  def project_cache_dir(%Project{} = project), do: project_cache_dir(project.id)

  def project_cache_dir(project_id) when is_binary(project_id) do
    Path.join([project_cache_root(), "projects", project_id])
  end

  def create_project(attrs) do
    now = Persistence.now_ms()
    name = attr(attrs, :name) || ""
    slug = unique_slug(attr(attrs, :slug) || Slug.slugify(name))

    Repo.transaction(fn ->
      project =
        %Project{}
        |> Project.changeset(%{
          id: Ecto.UUID.generate(),
          name: name,
          slug: slug,
          description: attr(attrs, :description),
          data_path: attr(attrs, :data_path),
          created_at: now,
          updated_at: now,
          is_active: false
        })
        |> Repo.insert!()

      {:ok, _templates} = Templates.rebuild_templates_from_files(project.id)
      project
    end)
    |> case do
      {:ok, project} -> sync_filesystem_metadata(project)
      {:error, reason} -> {:error, reason}
    end
  end

  def set_active_project(project_id) do
    case Repo.get(Project, project_id) do
      nil ->
        {:error, :not_found}

      project ->
        now = Persistence.now_ms()

        Repo.transaction(fn ->
          Repo.update_all(
            from(p in Project, where: p.is_active == true),
            set: [is_active: false, updated_at: now]
          )

          project
          |> Project.changeset(%{is_active: true, updated_at: now})
          |> Repo.update!()
        end)
        |> case do
          {:ok, active_project} -> {:ok, active_project}
          {:error, reason} -> {:error, reason}
        end
    end
  end

  def delete_project(project_id) when is_binary(project_id) do
    case Repo.get(Project, project_id) do
      nil ->
        {:error, :not_found}

      %Project{id: @default_project_id} ->
        {:error, :cannot_delete_default_project}

      %Project{is_active: true} ->
        {:error, :cannot_delete_active_project}

      %Project{} = project ->
        internal_dir = if is_nil(project.data_path), do: project_data_dir(project), else: nil
        cleanup_dirs = [internal_dir, project_cache_dir(project)] |> Enum.filter(&is_binary/1) |> Enum.uniq()

        Repo.transaction(fn ->
          Repo.delete!(project)
          project
        end)
        |> case do
          {:ok, deleted_project} ->
            Enum.each(cleanup_dirs, fn dir ->
              _ = File.rm_rf(dir)
            end)

            {:ok, deleted_project}

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

  defp project_summary(%Project{} = project) do
    %{
      id: project.id,
      name: project.name,
      slug: project.slug,
      data_path: project.data_path,
      is_active: project.is_active
    }
  end

  defp sync_filesystem_metadata(%Project{data_path: nil} = project), do: {:ok, project}

  defp sync_filesystem_metadata(%Project{} = project) do
    case Metadata.sync_project_metadata_from_filesystem(project.id) do
      {:ok, _metadata} -> {:ok, get_project!(project.id)}
      {:error, reason} -> {:error, reason}
    end
  end

  defp unique_slug(base_slug) do
    normalized = if base_slug in [nil, ""], do: "project", else: base_slug

    if slug_available?(normalized) do
      normalized
    else
      find_unique_slug(normalized, 2)
    end
  end

  defp find_unique_slug(base_slug, suffix) do
    candidate = "#{base_slug}-#{suffix}"

    if slug_available?(candidate) do
      candidate
    else
      find_unique_slug(base_slug, suffix + 1)
    end
  end

  defp slug_available?(slug) do
    not Repo.exists?(from project in Project, where: project.slug == ^slug)
  end

  defp repo_data_dir do
    Application.fetch_env!(:bds, BDS.Repo)
    |> Keyword.fetch!(:database)
    |> Path.expand()
    |> Path.dirname()
  end

  defp project_cache_root do
    case Application.get_env(:bds, :project_cache_root) do
      root when is_binary(root) -> Path.expand(root)
      _other -> repo_data_dir()
    end
  end

  defp attr(attrs, key) do
    cond do
      Map.has_key?(attrs, key) -> Map.get(attrs, key)
      Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
      true -> nil
    end
  end
end
