defmodule Mv.Membership.Setting do
  @moduledoc """
  Ash resource representing global application settings.

  ## Overview
  Settings is a singleton resource that stores global configuration for the association,
  such as the club name, branding information, and membership fee settings. There should
  only ever be one settings record in the database.

  ## Attributes
  - `club_name` - The name of the association/club (required, cannot be empty)
  - `member_field_visibility` - JSONB map storing visibility configuration for member fields
    (e.g., `%{"street" => false, "house_number" => false}`). Fields not in the map default to `true`.
  - `include_joining_cycle` - Whether to include the joining cycle in membership fee generation (default: true)
  - `default_membership_fee_type_id` - Default membership fee type for new members (optional)

  ## Singleton Pattern
  This resource uses a singleton pattern - there should only be one settings record.
  The resource is designed to be read and updated, but not created or destroyed
  through normal CRUD operations. Initial settings should be seeded.

  ## Environment Variable Support
  The `club_name` can be set via the `ASSOCIATION_NAME` environment variable.
  If set, the environment variable value is used as a fallback when no database
  value exists. Database values always take precedence over environment variables.

  ## Membership Fee Settings
  - `include_joining_cycle`: When true, members pay from their joining cycle. When false,
    they pay from the next full cycle after joining.
  - `default_membership_fee_type_id`: The membership fee type automatically assigned to
    new members. Can be nil if no default is set.

  ## Examples

      # Get current settings
      {:ok, settings} = Mv.Membership.get_settings()
      settings.club_name  # => "My Club"

      # Update club name
      {:ok, updated} = Mv.Membership.update_settings(settings, %{club_name: "New Name"})

      # Update member field visibility
      {:ok, updated} = Mv.Membership.update_member_field_visibility(settings, %{"street" => false, "house_number" => false})

      # Update membership fee settings
      {:ok, updated} = Mv.Membership.update_settings(settings, %{include_joining_cycle: false})
  """
  use Ash.Resource,
    domain: Mv.Membership,
    data_layer: AshPostgres.DataLayer

  postgres do
    table "settings"
    repo Mv.Repo
  end

  resource do
    description "Global application settings (singleton resource)"
  end

  actions do
    defaults [:read]

    # Internal create action - not exposed via code interface
    # Used only as fallback in get_settings/0 if settings don't exist
    # Settings should normally be created via seed script
    create :create do
      accept [
        :club_name,
        :member_field_visibility,
        :include_joining_cycle,
        :default_membership_fee_type_id,
        :vereinfacht_api_url,
        :vereinfacht_api_key,
        :vereinfacht_club_id,
        :vereinfacht_app_url
      ]
    end

    update :update do
      primary? true
      require_atomic? false

      accept [
        :club_name,
        :member_field_visibility,
        :include_joining_cycle,
        :default_membership_fee_type_id,
        :vereinfacht_api_url,
        :vereinfacht_api_key,
        :vereinfacht_club_id,
        :vereinfacht_app_url
      ]
    end

    update :update_member_field_visibility do
      description "Updates the visibility configuration for member fields in the overview"
      require_atomic? false
      accept [:member_field_visibility]
    end

    update :update_single_member_field_visibility do
      description "Atomically updates a single field in the member_field_visibility JSONB map"
      require_atomic? false

      argument :field, :string, allow_nil?: false
      argument :show_in_overview, :boolean, allow_nil?: false

      change Mv.Membership.Setting.Changes.UpdateSingleMemberFieldVisibility
    end

    update :update_membership_fee_settings do
      description "Updates the membership fee configuration"
      require_atomic? false
      accept [:include_joining_cycle, :default_membership_fee_type_id]

      change Mv.Membership.Setting.Changes.NormalizeDefaultFeeTypeId
    end
  end

  validations do
    validate present(:club_name), on: [:create, :update]
    validate string_length(:club_name, min: 1), on: [:create, :update]

    # Validate member_field_visibility map structure and content
    validate fn changeset, _context ->
               visibility = Ash.Changeset.get_attribute(changeset, :member_field_visibility)

               if visibility && is_map(visibility) do
                 # Validate all values are booleans
                 invalid_values =
                   Enum.filter(visibility, fn {_key, value} ->
                     not is_boolean(value)
                   end)

                 # Validate all keys are valid member fields
                 valid_field_strings = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)

                 invalid_keys =
                   Enum.filter(visibility, fn {key, _value} ->
                     key not in valid_field_strings
                   end)
                   |> Enum.map(fn {key, _value} -> key end)

                 cond do
                   not Enum.empty?(invalid_values) ->
                     {:error,
                      field: :member_field_visibility,
                      message: "All values in member_field_visibility must be booleans"}

                   not Enum.empty?(invalid_keys) ->
                     {:error,
                      field: :member_field_visibility,
                      message: "Invalid member field keys: #{inspect(invalid_keys)}"}

                   true ->
                     :ok
                 end
               else
                 :ok
               end
             end,
             on: [:create, :update]

    # Validate default_membership_fee_type_id exists if set
    validate fn changeset, context ->
               fee_type_id =
                 Ash.Changeset.get_attribute(changeset, :default_membership_fee_type_id)

               if fee_type_id do
                 # Check existence only; action is already restricted by policy (e.g. admin).
                 opts = [domain: Mv.MembershipFees, authorize?: false]

                 case Ash.get(Mv.MembershipFees.MembershipFeeType, fee_type_id, opts) do
                   {:ok, _} ->
                     :ok

                   {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} ->
                     {:error,
                      field: :default_membership_fee_type_id,
                      message: "Membership fee type not found"}

                   {:error, err} ->
                     # Log unexpected errors (DB timeout, connection errors, etc.)
                     require Logger

                     Logger.warning(
                       "Unexpected error when validating default_membership_fee_type_id: #{inspect(err)}"
                     )

                     # Return generic error to user
                     {:error,
                      field: :default_membership_fee_type_id,
                      message: "Could not validate membership fee type"}
                 end
               else
                 # Optional, can be nil
                 :ok
               end
             end,
             on: [:create, :update]
  end

  attributes do
    uuid_primary_key :id

    attribute :club_name, :string,
      allow_nil?: false,
      public?: true,
      description: "The name of the association/club",
      constraints: [
        trim?: true,
        min_length: 1
      ]

    attribute :member_field_visibility, :map,
      allow_nil?: true,
      public?: true,
      description:
        "Configuration for member field visibility in overview (JSONB map). Keys are member field names (atoms), values are booleans."

    # Membership fee settings
    attribute :include_joining_cycle, :boolean do
      allow_nil? false
      default true
      public? true
      description "Whether to include the joining cycle in membership fee generation"
    end

    attribute :default_membership_fee_type_id, :uuid do
      allow_nil? true
      public? true
      description "Default membership fee type ID for new members"
    end

    # Vereinfacht accounting software integration (can be overridden by ENV)
    attribute :vereinfacht_api_url, :string do
      allow_nil? true
      public? true
      description "Vereinfacht API base URL (e.g. https://api.verein.visuel.dev/api/v1)"
    end

    attribute :vereinfacht_api_key, :string do
      allow_nil? true
      public? true
      description "Vereinfacht API key (Bearer token)"
      sensitive? true
    end

    attribute :vereinfacht_club_id, :string do
      allow_nil? true
      public? true
      description "Vereinfacht club ID for multi-tenancy"
    end

    attribute :vereinfacht_app_url, :string do
      allow_nil? true
      public? true

      description "Vereinfacht app base URL for contact view links (e.g. https://app.verein.visuel.dev)"
    end

    timestamps()
  end

  relationships do
    # Optional relationship to the default membership fee type
    # Note: We use manual FK (default_membership_fee_type_id attribute) instead of belongs_to
    # to avoid circular dependency between Membership and MembershipFees domains
  end
end
