defmodule Towerops.Devices.CredentialResolver do
  @moduledoc """
  Resolves and denormalizes credentials for devices from organization/site hierarchy.

  This module populates device credential fields during create/update by resolving from
  the organization/site hierarchy and storing the resolved values directly on the device.

  ## Design Philosophy

  Instead of runtime cascading (device → site → org), we:
  1. Resolve credentials ONCE during device create/update
  2. Store resolved values directly on device fields
  3. Track source in `*_source` fields ("organization", "site", "device")
  4. Propagate changes when org/site credentials change

  ## Benefits

  - **Performance**: No need to preload site/org on every access
  - **Simplicity**: Just read `device.snmp_community` directly at runtime
  - **Clear audit**: Source fields show where credentials came from
  - **Easy propagation**: Find devices by source and update them

  ## Usage

  ### During device create/update:

      # In changeset pipeline
      changeset
      |> resolve_snmp_credentials(organization, site)
      |> resolve_mikrotik_credentials(organization, site)
      |> resolve_snmpv3_credentials(organization, site)

  ### At runtime (SNMP polling, etc):

      # Just read the fields directly - already resolved!
      device.snmp_community  # "public" (from org)
      device.snmp_community_source  # "organization"

  ### When org/site credentials change:

      # Propagate to devices that inherit from that source
      Devices.propagate_organization_community_change(org_id, new_community)
  """

  import Ecto.Changeset

  alias Towerops.Organizations.Organization
  alias Towerops.Sites.Site

  @doc """
  Resolves SNMP v2c credentials for a device changeset.

  Populates snmp_community, snmp_version, snmp_port on the device if they're not
  explicitly set (or empty), using site → organization fallback.

  Updates snmp_community_source to track where the community came from.
  """
  @spec resolve_snmp_credentials(Ecto.Changeset.t(), Organization.t(), Site.t() | nil) ::
          Ecto.Changeset.t()
  def resolve_snmp_credentials(changeset, organization, site) do
    # If user explicitly set community, keep it and mark as "device"
    # Otherwise resolve from site → org and track source
    changeset
    |> get_field(:snmp_community)
    |> resolve_snmp_community(changeset, organization, site)
    |> resolve_snmp_version(organization, site)
    |> resolve_snmp_port(organization, site)
  end

  defp resolve_snmp_version(changeset, organization, site) do
    case get_field(changeset, :snmp_version) do
      nil ->
        version = resolve_value(site, organization, :snmp_version) || "2c"
        put_change(changeset, :snmp_version, version)

      _value ->
        changeset
    end
  end

  defp resolve_snmp_port(changeset, organization, site) do
    case get_field(changeset, :snmp_port) do
      nil ->
        port = resolve_value(site, organization, :snmp_port) || 161
        put_change(changeset, :snmp_port, port)

      _value ->
        changeset
    end
  end

  @doc """
  Resolves SNMPv3 credentials for a device changeset.

  Populates SNMPv3 fields (username, security_level, auth_protocol, etc.) if not set,
  using site → organization fallback.

  Updates snmpv3_credential_source to track where credentials came from.
  """
  @spec resolve_snmpv3_credentials(Ecto.Changeset.t(), Organization.t(), Site.t() | nil) ::
          Ecto.Changeset.t()
  def resolve_snmpv3_credentials(changeset, organization, site) do
    changeset
    |> get_field(:snmp_version)
    |> do_resolve_snmpv3(changeset, organization, site)
  end

  @doc """
  Resolves MikroTik API credentials for a device changeset.

  Populates MikroTik fields (username, password, port, use_ssl) if not set,
  using site → organization fallback.

  Updates mikrotik_credential_source to track where credentials came from.
  """
  @spec resolve_mikrotik_credentials(Ecto.Changeset.t(), Organization.t(), Site.t() | nil) ::
          Ecto.Changeset.t()
  def resolve_mikrotik_credentials(changeset, organization, site) do
    # If user explicitly set username, keep it and mark as "device"
    # Otherwise resolve from site → org and track source
    case get_field(changeset, :mikrotik_username) do
      nil ->
        # Resolve from hierarchy
        {username, source} = resolve_with_source(site, organization, :mikrotik_username)
        password = resolve_value(site, organization, :mikrotik_password)
        port = resolve_value(site, organization, :mikrotik_port) || 8729
        ssh_port = resolve_value(site, organization, :mikrotik_ssh_port) || 22
        use_ssl = resolve_value(site, organization, :mikrotik_use_ssl)
        use_ssl = if use_ssl == nil, do: true, else: use_ssl
        enabled = resolve_value(site, organization, :mikrotik_enabled) || false

        changeset
        |> put_change(:mikrotik_username, username)
        |> put_change(:mikrotik_password, password)
        |> put_change(:mikrotik_port, port)
        |> put_change(:mikrotik_ssh_port, ssh_port)
        |> put_change(:mikrotik_use_ssl, use_ssl)
        |> put_change(:mikrotik_enabled, enabled)
        |> put_change(:mikrotik_credential_source, source)

      _value ->
        # User set a value, mark as device-specific
        put_change(changeset, :mikrotik_credential_source, "device")
    end
  end

  # Private helpers

  # Resolves SNMP community from field value, collapsing nil and "" (both mean "inherit")
  defp resolve_snmp_community(value, changeset, organization, site) when value in [nil, ""] do
    {community, source} = resolve_with_source(site, organization, :snmp_community)

    changeset
    |> put_change(:snmp_community, community)
    |> put_change(:snmp_community_source, source)
  end

  defp resolve_snmp_community(_value, changeset, _organization, _site) do
    put_change(changeset, :snmp_community_source, "device")
  end

  # Only resolve SNMPv3 fields when version is "3"
  defp do_resolve_snmpv3("3", changeset, organization, site) do
    case get_field(changeset, :snmpv3_username) do
      nil ->
        {username, source} = resolve_with_source(site, organization, :snmpv3_username)
        security_level = resolve_value(site, organization, :snmpv3_security_level)
        auth_protocol = resolve_value(site, organization, :snmpv3_auth_protocol) || "SHA-256"
        auth_password = resolve_value(site, organization, :snmpv3_auth_password)
        priv_protocol = resolve_value(site, organization, :snmpv3_priv_protocol) || "AES"
        priv_password = resolve_value(site, organization, :snmpv3_priv_password)

        changeset
        |> put_change(:snmpv3_username, username)
        |> put_change(:snmpv3_security_level, security_level)
        |> put_change(:snmpv3_auth_protocol, auth_protocol)
        |> put_change(:snmpv3_auth_password, auth_password)
        |> put_change(:snmpv3_priv_protocol, priv_protocol)
        |> put_change(:snmpv3_priv_password, priv_password)
        |> put_change(:snmpv3_credential_source, source)

      _value ->
        put_change(changeset, :snmpv3_credential_source, "device")
    end
  end

  defp do_resolve_snmpv3(_version, changeset, _organization, _site), do: changeset

  # Resolves a field from site → org, returns {value, source}
  defp resolve_with_source(%Site{} = site, organization, field) do
    case Map.get(site, field) do
      nil -> resolve_with_source(nil, organization, field)
      value -> {value, "site"}
    end
  end

  defp resolve_with_source(_site, %Organization{} = organization, field) do
    case Map.get(organization, field) do
      nil -> {nil, "organization"}
      value -> {value, "organization"}
    end
  end

  defp resolve_with_source(_site, _organization, _field), do: {nil, "organization"}

  # Resolves a field from site → org, returns just the value
  defp resolve_value(%Site{} = site, organization, field) do
    case Map.get(site, field) do
      nil -> resolve_value(nil, organization, field)
      value -> value
    end
  end

  defp resolve_value(_site, organization, field), do: Map.get(organization, field)
end
