defmodule CentralcloudCore.Security do
  @moduledoc """
  Client for the endpoint security monitoring platform.

  Provides customer-scoped agent listing, vulnerability counts, alert queries,
  SCA compliance results, and HostBill service-gate logic for the Security Center.

  Customers are grouped by the convention `cc_client_<hostbill_customer_id>`.
  """

  alias CentralcloudCore.HostBill

  # ---------------------------------------------------------------------------
  # Public API — agents
  # ---------------------------------------------------------------------------

  @doc "Return all monitored endpoints for a given customer."
  def get_monitored_agents(customer_id) when is_integer(customer_id) do
    with {:ok, token} <- authenticate() do
      result =
        do_get(token, "/agents", %{
          groups: customer_group(customer_id),
          limit: 500,
          select: "id,name,status,ip,version,lastKeepAlive,os.name,os.platform"
        })

      case result do
        {:ok, %{"data" => %{"affected_items" => items}}} -> {:ok, items}
        {:ok, _} -> {:ok, []}
        err -> err
      end
    end
  end

  @doc """
  List agents with optional scoping.

  Options:
  - `:customer_id` — integer, scopes to cc_client_{id} group
  - `:status`      — string, e.g. "active", "disconnected"
  - `:limit`       — integer, max results (default 100, max 500)
  """
  def list_agents(opts \\ []) do
    customer_id = Keyword.get(opts, :customer_id)
    status      = Keyword.get(opts, :status)
    limit       = min(Keyword.get(opts, :limit, 100), 500)

    params =
      %{
        limit: limit,
        select: "id,name,status,ip,version,lastKeepAlive,os.name,os.platform,groups"
      }
      |> maybe_put(:groups, customer_id && customer_group(customer_id))
      |> maybe_put(:status, status)

    with {:ok, token} <- authenticate(),
         {:ok, body} <- do_get(token, "/agents", params) do
      items = get_in(body, ["data", "affected_items"]) || []
      total = get_in(body, ["data", "total_affected_items"]) || length(items)
      {:ok, %{total: total, agents: items}}
    end
  end

  @doc "Connectivity health summary: active / disconnected / never_connected counts."
  def agent_summary(opts \\ []) do
    with {:ok, %{agents: agents}} <- list_agents(Keyword.put(opts, :limit, 500)) do
      by_status = Enum.frequencies_by(agents, & &1["status"])

      disconnected =
        Enum.filter(agents, &(&1["status"] in ["disconnected", "never_connected"]))
        |> Enum.map(&Map.take(&1, ["id", "name", "ip", "status", "lastKeepAlive"]))

      {:ok, %{total: length(agents), by_status: by_status, disconnected_agents: disconnected}}
    end
  end

  # ---------------------------------------------------------------------------
  # Public API — vulnerabilities
  # ---------------------------------------------------------------------------

  @doc """
  Return vulnerability counts by severity for a specific agent ID.
  """
  def get_agent_vulnerability_counts(agent_id) do
    with {:ok, token} <- authenticate() do
      result =
        do_get(token, "/vulnerability/#{agent_id}", %{
          limit: 500,
          distinct: true,
          select: "cve,severity"
        })

      case result do
        {:ok, %{"data" => %{"affected_items" => items}}} ->
          counts =
            Enum.frequencies_by(items, fn item ->
              String.downcase(item["severity"] || "unknown")
            end)

          {:ok, counts}

        _ ->
          {:ok, %{}}
      end
    end
  end

  @doc """
  Return CVE vulnerabilities for a specific agent.

  Options:
  - `:severity` — "Critical", "High", "Medium", "Low"
  - `:limit`    — max results (default 100)
  """
  def get_vulnerabilities(agent_id, opts \\ []) do
    severity = Keyword.get(opts, :severity)
    limit    = min(Keyword.get(opts, :limit, 100), 500)

    params =
      %{
        limit: limit,
        distinct: true,
        select: "cve,severity,cvss3_score,cvss2_score,name,version,architecture,title,published"
      }
      |> maybe_put(:severity, severity)

    with {:ok, token} <- authenticate(),
         {:ok, body} <- do_get(token, "/vulnerability/#{agent_id}", params) do
      items = get_in(body, ["data", "affected_items"]) || []
      total = get_in(body, ["data", "total_affected_items"]) || length(items)
      {:ok, %{agent_id: agent_id, total: total, vulnerabilities: items}}
    end
  end

  @doc """
  Top N critical CVEs across the fleet (or a customer), ranked by
  affected-host count then CVSS score.
  """
  def top_critical_cves(opts \\ []) do
    top_n = Keyword.get(opts, :top_n, 20)

    with {:ok, %{agents: agents}} <- list_agents(Keyword.put(opts, :status, "active") |> Keyword.put(:limit, 500)) do
      cve_map =
        Enum.reduce(agents, %{}, fn agent, acc ->
          aid = agent["id"]

          case get_vulnerabilities(aid, severity: "Critical", limit: 500) do
            {:ok, %{vulnerabilities: vulns}} ->
              Enum.reduce(vulns, acc, fn v, inner ->
                cve_id = v["cve"]

                if cve_id do
                  entry =
                    Map.get_lazy(inner, cve_id, fn ->
                      %{
                        cve: cve_id,
                        title: v["title"],
                        severity: v["severity"],
                        cvss3_score: v["cvss3_score"],
                        cvss2_score: v["cvss2_score"],
                        affected_agents: []
                      }
                    end)

                  updated =
                    Map.update!(entry, :affected_agents, fn agents ->
                      [%{id: aid, name: agent["name"]} | agents]
                    end)

                  Map.put(inner, cve_id, updated)
                else
                  inner
                end
              end)

            _ ->
              acc
          end
        end)

      ranked =
        cve_map
        |> Map.values()
        |> Enum.sort_by(
          fn c ->
            {length(c.affected_agents),
             c.cvss3_score || c.cvss2_score || 0.0}
          end,
          :desc
        )
        |> Enum.take(top_n)

      {:ok,
       %{
         scanned_agents: length(agents),
         unique_critical_cves: map_size(cve_map),
         top_cves: ranked
       }}
    end
  end

  # ---------------------------------------------------------------------------
  # Public API — alerts
  # ---------------------------------------------------------------------------

  @doc """
  Return recent SIEM alerts.

  Options:
  - `:customer_id` — scope to a customer's agents
  - `:agent_id`    — scope to a single agent
  - `:level`       — minimum rule level (default 7)
  - `:limit`       — max results (default 50)
  """
  def get_alerts(opts \\ []) do
    customer_id = Keyword.get(opts, :customer_id)
    agent_id    = Keyword.get(opts, :agent_id)
    level       = Keyword.get(opts, :level, 7)
    limit       = min(Keyword.get(opts, :limit, 50), 500)

    params = %{
      limit: limit,
      sort: "-timestamp",
      select:
        "id,timestamp,agent.id,agent.name,rule.id,rule.description,rule.level,rule.groups,data.srcip,data.dstuser",
      level: "#{level}-15"
    }

    with {:ok, token} <- authenticate() do
      params =
        cond do
          agent_id ->
            Map.put(params, :agents_list, agent_id)

          customer_id ->
            case list_agents(customer_id: customer_id, limit: 500) do
              {:ok, %{agents: agents}} ->
                ids = Enum.map_join(agents, ",", & &1["id"])
                if ids == "", do: :empty, else: Map.put(params, :agents_list, ids)

              _ ->
                params
            end

          true ->
            params
        end

      case params do
        :empty ->
          {:ok, %{total: 0, alerts: []}}

        params ->
          case do_get(token, "/alerts", params) do
            {:ok, body} ->
              items = get_in(body, ["data", "affected_items"]) || []
              total = get_in(body, ["data", "total_affected_items"]) || length(items)
              {:ok, %{total: total, alerts: items}}

            err ->
              err
          end
      end
    end
  end

  # ---------------------------------------------------------------------------
  # Public API — SCA (CIS compliance)
  # ---------------------------------------------------------------------------

  @doc """
  Return SCA/CIS benchmark results for an agent.

  If `policy_id` is given, returns individual check results for that policy.
  Otherwise returns all policy summaries.
  """
  def get_sca(agent_id, opts \\ []) do
    policy_id = Keyword.get(opts, :policy_id)

    with {:ok, token} <- authenticate() do
      if policy_id do
        case do_get(token, "/sca/#{agent_id}/checks/#{policy_id}", %{limit: 500}) do
          {:ok, body} ->
            items  = get_in(body, ["data", "affected_items"]) || []
            failed = Enum.filter(items, &(&1["result"] == "failed"))

            {:ok,
             %{
               agent_id: agent_id,
               policy_id: policy_id,
               total_checks: length(items),
               failed_checks: length(failed),
               failed: failed
             }}

          err ->
            err
        end
      else
        case do_get(token, "/sca/#{agent_id}", %{limit: 100}) do
          {:ok, body} ->
            policies = get_in(body, ["data", "affected_items"]) || []
            {:ok, %{agent_id: agent_id, policies: policies}}

          err ->
            err
        end
      end
    end
  end

  # ---------------------------------------------------------------------------
  # HostBill service gating
  # ---------------------------------------------------------------------------

  @doc """
  Return true when the list of HostBill service products includes a security
  monitoring subscription.  Matches product names containing "security",
  "threat", or "monitor" (case-insensitive).
  """
  def has_security_service?(services) when is_list(services) do
    Enum.any?(services, fn svc ->
      name = String.downcase(svc["name"] || "")
      String.contains?(name, "security") or
        String.contains?(name, "threat") or
        String.contains?(name, "monitor")
    end)
  end

  def has_security_service?(_), do: false

  @doc "Fetch HostBill services for a customer and gate on security service."
  def customer_has_security_service?(customer_id) when is_integer(customer_id) do
    case HostBill.get_client_services(customer_id) do
      {:ok, %{"products" => products}} -> has_security_service?(products)
      _ -> :unavailable
    end
  end

  @doc "The monitoring platform group name for a customer."
  def customer_group(customer_id), do: "cc_client_#{customer_id}"

  # ---------------------------------------------------------------------------
  # Private helpers
  # ---------------------------------------------------------------------------

  defp authenticate do
    cfg = config()
    url = cfg[:url]
    credential = Base.encode64("#{cfg[:username]}:#{cfg[:password]}")

    case Req.post(
           "#{url}/security/user/authenticate?raw=true",
           headers: [{"authorization", "Basic #{credential}"}],
           connect_options: ssl_opts(cfg)
         ) do
      {:ok, %{status: 200, body: token}} when is_binary(token) ->
        {:ok, String.trim(token)}

      {:ok, %{status: status}} ->
        {:error, {:auth_failed, status}}

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

  defp do_get(token, path, params) do
    cfg = config()

    case Req.get(
           "#{cfg[:url]}#{path}",
           headers: [{"authorization", "Bearer #{token}"}],
           params: params,
           connect_options: ssl_opts(cfg)
         ) do
      {:ok, %{status: 200, body: body}} -> {:ok, body}
      {:ok, %{status: status}} -> {:error, {:http, status}}
      {:error, reason} -> {:error, {:connect_error, reason}}
    end
  end

  defp ssl_opts(cfg) do
    if cfg[:verify_ssl] == false do
      [transport_opts: [verify: :verify_none]]
    else
      []
    end
  end

  defp maybe_put(map, _key, nil), do: map
  defp maybe_put(map, key, value), do: Map.put(map, key, value)

  defp config, do: Application.fetch_env!(:centralcloud_core, :security)
end
