defmodule BDS.MCP.Tools do
  @moduledoc false

  import Ecto.Query
  import BDS.MCP.Util, only: [maybe_put: 3, map_get: 3, sanitize: 1, normalize_term: 1]

  alias BDS.MCP.ProposalStore
  alias BDS.MCP.Queries
  alias BDS.Media
  alias BDS.Media.Media, as: MediaAsset
  alias BDS.Posts
  alias BDS.Posts.Post
  alias BDS.Repo
  alias BDS.Scripts
  alias BDS.Search
  alias BDS.Templates

  @proposal_ttl_app_ms 30 * 60 * 1000
  @tools [
    {"check_term", true},
    {"search_posts", true},
    {"count_posts", true},
    {"read_post_by_slug", true},
    {"get_post_translations", true},
    {"get_media_translations", true},
    {"upsert_media_translation", false},
    {"draft_post", false},
    {"propose_script", false},
    {"propose_template", false},
    {"propose_media_metadata", false},
    {"propose_post_metadata", false},
    {"accept_proposal", false},
    {"discard_proposal", false}
  ]

  @typedoc "Tool descriptor returned by `list/0`."
  @type descriptor :: %{
          name: String.t(),
          title: String.t(),
          description: String.t(),
          inputSchema: map(),
          annotations: map()
        }

  @spec list() :: [descriptor()]
  def list do
    Enum.map(@tools, fn {name, read_only} -> tool(name, read_only) end)
  end

  @spec call(String.t(), map()) :: {:ok, term()} | {:error, term()}
  def call(name, params) when is_binary(name) and is_map(params) do
    ProposalStore.ensure_started()

    case name do
      "check_term" -> {:ok, check_term(params)}
      "search_posts" -> {:ok, search_posts(params)}
      "count_posts" -> {:ok, count_posts(params)}
      "read_post_by_slug" -> read_post_by_slug(params)
      "get_post_translations" -> get_post_translations(params)
      "get_media_translations" -> get_media_translations(params)
      "upsert_media_translation" -> upsert_media_translation(params)
      "draft_post" -> draft_post(params)
      "propose_script" -> propose_script(params)
      "propose_template" -> propose_template(params)
      "propose_media_metadata" -> propose_media_metadata(params)
      "propose_post_metadata" -> propose_post_metadata(params)
      "accept_proposal" -> accept_proposal(params)
      "discard_proposal" -> discard_proposal(params)
      _other -> {:error, :unknown_tool}
    end
  end

  @spec validate_template(String.t()) :: {:ok, %{valid: boolean(), errors: [String.t()]}}
  def validate_template(source) when is_binary(source) do
    case BDS.Rendering.LiquidParser.validate(source) do
      {:ok, _ast} ->
        {:ok, %{valid: true, errors: []}}

      {:error, reason, line} ->
        {:ok, %{valid: false, errors: ["#{inspect(reason)} at line #{line}"]}}
    end
  end

  defp tool(name, read_only) do
    metadata = tool_metadata(name)

    %{
      name: name,
      title: metadata.title,
      description: metadata.description,
      inputSchema: metadata.input_schema,
      annotations: %{
        "readOnlyHint" => read_only,
        "destructiveHint" => false,
        "openWorldHint" => false
      }
    }
  end

  defp tool_metadata("check_term") do
    single_string_arg_tool_metadata(
      "Check Term",
      "Check whether a term exists as a category, tag, or both. Returns post counts for each. Use before search_posts or count_posts when unsure whether a term is a category or tag.",
      "term",
      "The term to look up"
    )
  end

  defp tool_metadata("search_posts") do
    %{
      title: "Search Posts",
      description:
        "Search blog posts by query, category, tags, language, translation coverage, date, or status. Returns a paginated envelope with total, offset, limit, hasMore, and posts. Each post includes title, slug, tags, categories, backlinks, and linksTo. When hasMore is true, increase offset by limit. Use check_term first if unsure whether a term is a category or tag.",
      input_schema: post_query_schema(false)
    }
  end

  defp tool_metadata("count_posts") do
    %{
      title: "Count Posts",
      description:
        "Count posts grouped by year, month, tag, category, or status. Returns aggregated counts without full post data, useful for analytics, distributions, and heat maps. Example: groupBy=[\"month\",\"tag\"] with year=2004.",
      input_schema:
        object_schema(
          Map.merge(group_filter_properties(), %{
            "groupBy" => %{
              "type" => "array",
              "items" => enum_schema(["year", "month", "tag", "category", "status"]),
              "description" => "Dimensions to group by; one to three dimensions is usually best"
            }
          }),
          ["groupBy"]
        )
    }
  end

  defp tool_metadata("read_post_by_slug") do
    %{
      title: "Read Post By Slug",
      description:
        "Read full content and metadata for a specific blog post by slug. Includes title, excerpt, content, status, tags, categories, backlinks, linksTo, and available languages. Optionally request a translation by language.",
      input_schema:
        object_schema(
          %{
            "slug" => string_schema("The slug of the post to read"),
            "language" => string_schema("Optional language code for a translation")
          },
          ["slug"]
        )
    }
  end

  defp tool_metadata("get_post_translations") do
    single_string_arg_tool_metadata(
      "Get Post Translations",
      "List all translations available for a blog post, including language, title, excerpt, content, and status.",
      "postId",
      "The post ID"
    )
  end

  defp tool_metadata("get_media_translations") do
    single_string_arg_tool_metadata(
      "Get Media Translations",
      "List all available translations for media metadata, including language, title, alt text, and captions.",
      "mediaId",
      "The media ID"
    )
  end

  defp tool_metadata("upsert_media_translation") do
    %{
      title: "Upsert Media Translation",
      description: "Create or update translated media metadata for a specific language.",
      input_schema:
        object_schema(
          %{
            "mediaId" => string_schema("The media ID"),
            "language" => string_schema("Language code to update"),
            "title" => string_schema("Translated title"),
            "alt" => string_schema("Translated alt text"),
            "caption" => string_schema("Translated caption")
          },
          ["mediaId", "language"]
        )
    }
  end

  defp tool_metadata("draft_post") do
    %{
      title: "Draft Post",
      description: "Create a new draft blog post for review before publishing.",
      input_schema:
        object_schema(
          %{
            "title" => string_schema("Post title"),
            "content" => string_schema("Post content in Markdown"),
            "excerpt" => string_schema("Short excerpt or summary"),
            "tags" => string_array_schema("Tags for the post"),
            "categories" => string_array_schema("Categories for the post"),
            "author" => string_schema("Post author name")
          },
          ["title", "content"]
        )
    }
  end

  defp tool_metadata("propose_script") do
    %{
      title: "Propose Script",
      description: "Propose a new Python script, macro, utility, or transform for review.",
      input_schema:
        object_schema(
          %{
            "title" => string_schema("Script title"),
            "kind" => enum_schema(["macro", "utility", "transform"]),
            "content" => string_schema("Python source code"),
            "entrypoint" => string_schema("Entry point function name")
          },
          ["title", "kind", "content"]
        )
    }
  end

  defp tool_metadata("propose_template") do
    %{
      title: "Propose Template",
      description: "Propose a new Liquid template for review.",
      input_schema:
        object_schema(
          %{
            "title" => string_schema("Template title"),
            "kind" => enum_schema(["post", "list", "not-found", "partial"]),
            "content" => string_schema("Liquid template content")
          },
          ["title", "kind", "content"]
        )
    }
  end

  defp tool_metadata("propose_media_metadata") do
    %{
      title: "Propose Media Metadata",
      description:
        "Propose changes to media metadata such as title, alt text, caption, and tags.",
      input_schema:
        object_schema(
          %{
            "mediaId" => string_schema("The media ID"),
            "title" => string_schema("New title"),
            "alt" => string_schema("New alt text"),
            "caption" => string_schema("New caption"),
            "tags" => string_array_schema("New tags")
          },
          ["mediaId"]
        )
    }
  end

  defp tool_metadata("propose_post_metadata") do
    %{
      title: "Propose Post Metadata",
      description:
        "Propose changes to post metadata such as title, excerpt, tags, and categories.",
      input_schema:
        object_schema(
          %{
            "postId" => string_schema("The post ID"),
            "title" => string_schema("New title"),
            "excerpt" => string_schema("New excerpt"),
            "tags" => string_array_schema("New tags"),
            "categories" => string_array_schema("New categories")
          },
          ["postId"]
        )
    }
  end

  defp tool_metadata("accept_proposal") do
    single_string_arg_tool_metadata(
      "Accept Proposal",
      "Accept a pending proposal and apply or publish its changes.",
      "proposalId",
      "The proposal ID"
    )
  end

  defp tool_metadata("discard_proposal") do
    single_string_arg_tool_metadata(
      "Discard Proposal",
      "Discard a pending proposal and remove any temporary draft artifacts.",
      "proposalId",
      "The proposal ID"
    )
  end

  defp single_string_arg_tool_metadata(title, description, field, field_description) do
    %{
      title: title,
      description: description,
      input_schema: object_schema(%{field => string_schema(field_description)}, [field])
    }
  end

  defp post_query_schema(query_required) do
    required = if query_required, do: ["query"], else: []

    object_schema(
      Map.merge(group_filter_properties(), %{
        "query" => string_schema("Full-text search query"),
        "language" => string_schema("Require posts available in this language"),
        "missingTranslationLanguage" =>
          string_schema("Require posts missing this translation language"),
        "offset" => %{"type" => "integer", "minimum" => 0, "description" => "Pagination offset"},
        "limit" => %{
          "type" => "integer",
          "minimum" => 1,
          "maximum" => 50,
          "description" => "Maximum results to return"
        }
      }),
      required
    )
  end

  defp group_filter_properties do
    %{
      "year" => %{"type" => "integer", "description" => "Filter to posts in this year"},
      "month" => %{
        "type" => "integer",
        "minimum" => 1,
        "maximum" => 12,
        "description" => "Filter to posts in this month; requires year"
      },
      "status" => enum_schema(["draft", "published", "archived"]),
      "category" => string_schema("Filter by category"),
      "tags" => string_array_schema("Filter by tags; all must match")
    }
  end

  defp object_schema(properties, required) do
    %{"type" => "object", "properties" => properties}
    |> maybe_schema_required(required)
  end

  defp maybe_schema_required(schema, []), do: schema
  defp maybe_schema_required(schema, required), do: Map.put(schema, "required", required)

  defp string_schema(description), do: %{"type" => "string", "description" => description}

  defp string_array_schema(description),
    do: %{"type" => "array", "items" => %{"type" => "string"}, "description" => description}

  defp enum_schema(values), do: %{"type" => "string", "enum" => values}

  defp check_term(%{"term" => term}), do: check_term(%{term: term})

  defp check_term(%{term: term}) do
    project = Queries.active_project!()
    normalized = normalize_term(term)

    posts = Repo.all(from post in Post, where: post.project_id == ^project.id)

    tag_post_count =
      Enum.count(posts, fn post ->
        Enum.any?(post.tags || [], &(normalize_term(&1) == normalized))
      end)

    category_post_count =
      Enum.count(posts, fn post ->
        Enum.any?(post.categories || [], &(normalize_term(&1) == normalized))
      end)

    %{
      "is_category" => category_post_count > 0,
      "category_post_count" => category_post_count,
      "is_tag" => tag_post_count > 0,
      "tag_post_count" => tag_post_count
    }
  end

  defp search_posts(params) do
    project = Queries.active_project!()
    filters = Queries.search_filters(params)
    query = map_get(params, :query, "")
    {:ok, result} = Search.search_posts(project.id, query, filters)

    posts = Enum.map(result.posts, &Queries.post_summary/1)

    %{
      "posts" => posts,
      "total" => result.total,
      "offset" => result.offset,
      "limit" => result.limit,
      "has_more" => result.offset + result.limit < result.total
    }
  end

  defp count_posts(params) do
    project = Queries.active_project!()
    group_by = map_get(params, :groupBy, []) |> Enum.map(&to_string/1)
    result = search_all_counted_posts(project.id, params)

    groups =
      result.posts
      |> Enum.flat_map(&Queries.group_rows(&1, group_by))
      |> Enum.group_by(& &1, fn _row -> 1 end)
      |> Enum.map(fn {row, counts} -> Map.put(row, "count", length(counts)) end)
      |> Enum.sort_by(&Map.to_list/1)

    %{"groups" => groups, "total_posts" => result.total}
  end

  defp search_all_counted_posts(project_id, params) do
    filters = Queries.search_filters(params) |> Map.put(:offset, 0) |> Map.put(:limit, 1)
    {:ok, %{total: total}} = Search.search_posts(project_id, "", filters)

    filters = Map.put(filters, :limit, max(total, 1))
    {:ok, result} = Search.search_posts(project_id, "", filters)

    result
  end

  defp read_post_by_slug(%{"slug" => slug} = params),
    do: read_post_by_slug(Map.put_new(params, :slug, slug))

  defp read_post_by_slug(%{slug: slug} = params) do
    project = Queries.active_project!()

    case Repo.get_by(Post, project_id: project.id, slug: slug) do
      nil ->
        {:error, :not_found}

      %Post{} = post ->
        payload =
          case map_get(params, :language, nil) do
            nil ->
              Queries.post_detail(post)

            "" ->
              Queries.post_detail(post)

            language ->
              if normalize_term(language) == normalize_term(post.language) do
                Queries.post_detail(post)
              else
                Queries.translated_post_detail(post, language)
              end
          end

        {:ok, %{"post" => payload}}
    end
  end

  defp get_post_translations(params) do
    post_id = map_get(params, :postId, nil)

    case Posts.get_post(post_id) do
      nil ->
        {:error, :not_found}

      %Post{} ->
        with {:ok, translations} <- Posts.list_post_translations(post_id) do
          {:ok, %{"translations" => sanitize(translations)}}
        end
    end
  end

  defp get_media_translations(params) do
    media_id = map_get(params, :mediaId, nil)

    case Media.get_media(media_id) do
      nil ->
        {:error, :not_found}

      %MediaAsset{} ->
        {:ok, %{"translations" => sanitize(Media.list_media_translations(media_id))}}
    end
  end

  defp upsert_media_translation(params) do
    media_id = map_get(params, :mediaId, nil)
    language = params |> map_get(:language, "") |> normalize_term()

    attrs = %{
      title: map_get(params, :title, nil),
      alt: map_get(params, :alt, nil),
      caption: map_get(params, :caption, nil)
    }

    with {:ok, translation} <- Media.upsert_media_translation(media_id, language, attrs) do
      {:ok, %{"translation" => sanitize(translation)}}
    end
  end

  defp draft_post(params) do
    project = Queries.active_project!()

    attrs = %{
      project_id: project.id,
      title: map_get(params, :title, ""),
      content: map_get(params, :content, ""),
      excerpt: map_get(params, :excerpt, nil),
      tags: map_get(params, :tags, []),
      categories: map_get(params, :categories, []),
      author: map_get(params, :author, nil)
    }

    with {:ok, post} <- Posts.create_post(attrs) do
      proposal =
        ProposalStore.create("draft_post", %{"post_id" => post.id},
          entity_id: post.id,
          ttl_ms: @proposal_ttl_app_ms
        )

      {:ok, %{"proposal_id" => proposal.id, "post" => sanitize(post)}}
    end
  end

  defp propose_script(params) do
    project = Queries.active_project!()
    content = map_get(params, :content, "")

    with validation <- script_validation(content),
         {:ok, script} <-
           Scripts.create_script(%{
             project_id: project.id,
             title: map_get(params, :title, ""),
             kind: parse_script_kind(map_get(params, :kind, nil)),
             content: content,
             entrypoint: map_get(params, :entrypoint, nil)
           }) do
      proposal =
        ProposalStore.create("propose_script", %{"script_id" => script.id},
          entity_id: script.id,
          ttl_ms: @proposal_ttl_app_ms
        )

      {:ok,
       %{
         "proposal_id" => proposal.id,
         "script" => sanitize(script),
         "preview" => %{
           "title" => script.title,
           "kind" => Atom.to_string(script.kind),
           "content_length" => String.length(content),
           "syntax_valid" => validation.valid,
           "syntax_errors" => validation.errors
         }
       }}
    end
  end

  defp propose_template(params) do
    project = Queries.active_project!()
    content = map_get(params, :content, "")
    {:ok, validation} = validate_template(content)

    with {:ok, template} <-
           Templates.create_template(%{
             project_id: project.id,
             title: map_get(params, :title, ""),
             kind: parse_template_kind(map_get(params, :kind, nil)),
             content: content
           }) do
      proposal =
        ProposalStore.create("propose_template", %{"template_id" => template.id},
          entity_id: template.id,
          ttl_ms: @proposal_ttl_app_ms
        )

      {:ok,
       %{
         "proposal_id" => proposal.id,
         "template" => sanitize(template),
         "preview" => %{
           "title" => template.title,
           "kind" => Atom.to_string(template.kind),
           "content_length" => String.length(content),
           "syntax_valid" => validation.valid,
           "syntax_errors" => validation.errors
         }
       }}
    end
  end

  defp propose_media_metadata(params) do
    media_id = map_get(params, :mediaId, nil)

    case Repo.get(MediaAsset, media_id) do
      nil ->
        {:error, :not_found}

      %MediaAsset{} = media ->
        changes =
          %{}
          |> maybe_put("title", map_get(params, :title, nil))
          |> maybe_put("alt", map_get(params, :alt, nil))
          |> maybe_put("caption", map_get(params, :caption, nil))
          |> maybe_put("tags", map_get(params, :tags, nil))

        proposal =
          ProposalStore.create(
            "propose_media_metadata",
            %{"media_id" => media_id, "changes" => changes},
            entity_id: media_id,
            ttl_ms: @proposal_ttl_app_ms
          )

        {:ok,
         %{"proposal_id" => proposal.id, "current" => sanitize(media), "proposed" => changes}}
    end
  end

  defp propose_post_metadata(params) do
    post_id = map_get(params, :postId, nil)

    case Repo.get(Post, post_id) do
      nil ->
        {:error, :not_found}

      %Post{} = post ->
        changes =
          %{}
          |> maybe_put("title", map_get(params, :title, nil))
          |> maybe_put("excerpt", map_get(params, :excerpt, nil))
          |> maybe_put("tags", map_get(params, :tags, nil))
          |> maybe_put("categories", map_get(params, :categories, nil))

        proposal =
          ProposalStore.create(
            "propose_post_metadata",
            %{"post_id" => post_id, "changes" => changes},
            entity_id: post_id,
            ttl_ms: @proposal_ttl_app_ms
          )

        {:ok, %{"proposal_id" => proposal.id, "current" => sanitize(post), "proposed" => changes}}
    end
  end

  defp accept_proposal(params) do
    proposal_id = map_get(params, :proposalId, nil)

    case ProposalStore.get(proposal_id) do
      nil ->
        {:error, :not_found}

      proposal ->
        result =
          case proposal.kind do
            "draft_post" ->
              proposal.data["post_id"] |> Posts.publish_post()

            "propose_script" ->
              proposal.data["script_id"] |> Scripts.publish_script()

            "propose_template" ->
              proposal.data["template_id"] |> Templates.publish_template()

            "propose_media_metadata" ->
              Media.update_media(proposal.data["media_id"], proposal.data["changes"] || %{})

            "propose_post_metadata" ->
              Posts.update_post(proposal.data["post_id"], proposal.data["changes"] || %{})

            _other ->
              {:error, :unsupported_proposal}
          end

        case result do
          {:ok, value} ->
            _ = ProposalStore.mark_accepted(proposal_id)
            {:ok, %{"success" => true, "message" => "accepted", "result" => sanitize(value)}}

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

  defp discard_proposal(params) do
    proposal_id = map_get(params, :proposalId, nil)

    case ProposalStore.get(proposal_id) do
      nil ->
        {:error, :not_found}

      proposal ->
        result =
          case proposal.kind do
            "draft_post" -> Posts.delete_post(proposal.data["post_id"])
            "propose_script" -> Scripts.delete_script(proposal.data["script_id"])
            "propose_template" -> Templates.delete_template(proposal.data["template_id"])
            _other -> {:ok, :discarded}
          end

        case result do
          {:ok, _value} ->
            _ = ProposalStore.mark_discarded(proposal_id)
            {:ok, %{"success" => true, "message" => "discarded"}}

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

  defp script_validation(content) do
    case BDS.Scripting.validate(content) do
      :ok -> %{valid: true, errors: []}
      {:error, reason} -> %{valid: false, errors: [inspect(reason)]}
    end
  end

  defp parse_script_kind(value) when is_atom(value), do: value
  defp parse_script_kind("macro"), do: :macro
  defp parse_script_kind("utility"), do: :utility
  defp parse_script_kind("transform"), do: :transform

  defp parse_template_kind(value) when is_atom(value), do: value
  defp parse_template_kind("post"), do: :post
  defp parse_template_kind("list"), do: :list
  defp parse_template_kind("not-found"), do: :not_found
  defp parse_template_kind("not_found"), do: :not_found
  defp parse_template_kind("partial"), do: :partial
end
