#!/usr/bin/env bash
# identity.module.sh — identity file management and peer-name inference

declare -gA __empty_map=()

# ===========================================================================
# Path helpers
# ===========================================================================

function identity::path() {
  local name="${1:-}"
  echo "$(ctx::identities)/${name}.identity"
}

# ===========================================================================
# Existence checks
# ===========================================================================

function identity::exists() {
  local name="${1:-}"
  json::identity_exists "$(identity::path "$name")" 2>/dev/null
}

function identity::require_exists() {
  local name="${1:-}"
  if ! identity::exists "$name"; then
    log::error "Identity '${name}' not found. Use 'wgctl identity list' to see all identities."
    return 1
  fi
}

function identity::require_not_exists() {
  local name="${1:-}"
  if identity::exists "$name"; then
    log::error "Identity '${name}' already exists."
    return 1
  fi
}
 
# identity::require_exists_for_flag <identity_name>
# Used by commands to validate --identity value before proceeding.
function identity::require_exists_for_flag() {
  local identity_name="${1:-}"
  [[ -z "$identity_name" ]] && {
    log::error "Missing value for --identity"
    return 1
  }
  # Identity may not exist yet for add (it will be created)
  # Only require existence for commands that read from it
  return 0
}
 
# identity::require_has_peers <identity_name>
# Used by block/unblock/list to ensure identity has peers to operate on.
function identity::require_has_peers() {
  local identity_name="${1:-}"
  local peers
  peers=$(identity::peers "$identity_name")
  if [[ -z "$peers" ]]; then
    log::error "Identity '${identity_name}' has no peers"
    return 1
  fi
}

# ===========================================================================
# Peer name inference
# ===========================================================================

# identity::infer <peer_name>
# Parses a peer name and returns "identity_name|type|index" if it matches
# the naming convention, or empty string if not.
# phone-nuno   -> "nuno|phone|1"
# phone-nuno-2 -> "nuno|phone|2"
# roboclean    -> "" (no type prefix)
function identity::infer() {
  local peer_name="${1:-}"
  json::identity_infer "$peer_name" 2>/dev/null || true
}

# identity::next_index <identity_name> <type>
# Returns the next available device index for a type within an identity.
# If identity doesn't exist yet, returns 1.
function identity::next_index() {
  local identity_name="${1:-}" peer_type="${2:-}"
  local id_file
  id_file=$(identity::path "$identity_name")
  if [[ ! -f "$id_file" ]]; then
    echo 1
    return 0
  fi
  json::identity_next_index "$id_file" "$peer_type" 2>/dev/null || echo 1
}

# identity::next_peer_name <identity_name> <type>
# Returns the full peer name for the next device of a given type
# for an identity. Creates the name with the correct index.
# e.g. identity::next_peer_name helena phone → phone-helena-2
#      (if phone-helena already exists, index 1 is taken)
function identity::next_peer_name() {
  local identity_name="${1:-}" peer_type="${2:-}"
  [[ -z "$identity_name" || -z "$peer_type" ]] && return 1
 
  local index
  index=$(identity::next_index "$identity_name" "$peer_type")
 
  if [[ "$index" -eq 1 ]]; then
    echo "${peer_type}-${identity_name}"
  else
    echo "${peer_type}-${identity_name}-${index}"
  fi
}

# ===========================================================================
# Auto-attach (called from wgctl add)
# ===========================================================================

# identity::auto_attach <peer_name> <peer_type>
# Infers identity from peer name and adds the peer to the identity file.
# Creates the identity file if it doesn't exist.
# Silent — no output. Logs a note on success, silently skips if no match.
function identity::auto_attach() {
  local peer_name="${1:-}" peer_type="${2:-}"
  local inferred
  inferred=$(identity::infer "$peer_name")
  [[ -z "$inferred" ]] && return 0

  local identity_name type_inferred index
  identity_name=$(echo "$inferred" | cut -d'|' -f1)
  type_inferred=$(echo  "$inferred" | cut -d'|' -f2)
  index=$(echo          "$inferred" | cut -d'|' -f3)

  # Use the explicit type if provided, otherwise use inferred type
  local final_type="${peer_type:-$type_inferred}"

  local id_file
  id_file=$(identity::path "$identity_name")

  json::identity_add_peer "$id_file" "$identity_name" "$peer_name" "$final_type" "$index" </dev/null
  log::info "Attached '${peer_name}' to identity '${identity_name}' (${final_type} #${index})"
}

# identity::auto_detach <peer_name>
# Removes a peer from its identity file when the peer is deleted.
# If the identity has no remaining peers, removes the identity file too.
function identity::auto_detach() {
  local peer_name="${1:-}"
  local inferred
  inferred=$(identity::infer "$peer_name")
  [[ -z "$inferred" ]] && return 0

  local identity_name
  identity_name=$(echo "$inferred" | cut -d'|' -f1)
  local id_file
  id_file=$(identity::path "$identity_name")
  [[ ! -f "$id_file" ]] && return 0

  json::identity_remove_peer "$id_file" "$peer_name" </dev/null

  # Remove identity file if now empty
  local remaining
  remaining=$(json::identity_peers "$id_file" 2>/dev/null) || true
  if [[ -z "$remaining" ]]; then
    rm -f "$id_file"
    log::info "Identity '${identity_name}' removed (no remaining peers)"
  fi
}

# ===========================================================================
# Peer queries
# ===========================================================================

# identity::peers <identity_name> [type_filter]
# Returns peer names belonging to an identity, one per line.
# Optional type_filter limits to peers of a specific type.
function identity::peers() {
  local identity_name="${1:-}" type_filter="${2:-}"
  local id_file
  id_file=$(identity::path "$identity_name")
  json::identity_peers "$id_file" "$type_filter" 2>/dev/null || true
}

# identity::get_name <peer_name>
# Returns the identity name for a given peer (via inference).
function identity::get_name() {
  local peer_name="${1:-}"
  local inferred
  inferred=$(identity::infer "$peer_name")
  [[ -n "$inferred" ]] && echo "${inferred%%|*}"
}

# ===========================================================================
# Data for commands
# ===========================================================================

function identity::list_data() {
  json::identity_list "$(ctx::identities)" 2>/dev/null || true
}

function identity::show_data() {
  local name="${1:-}"
  json::identity_show "$(identity::path "$name")" 2>/dev/null
}

# ===========================================================================
# Rename helper (called from rename.command.sh)
# ===========================================================================

# identity::rename_peer <old_peer_name> <new_peer_name>
# Updates identity file entry when a peer is renamed.
# Re-infers identity from old name, removes old entry, adds new entry.
function identity::rename_peer() {
  local old_name="${1:-}" new_name="${2:-}"

  local old_inferred
  old_inferred=$(identity::infer "$old_name")
  [[ -z "$old_inferred" ]] && return 0

  local identity_name old_type old_index
  identity_name=$(echo "$old_inferred" | cut -d'|' -f1)
  old_type=$(echo      "$old_inferred" | cut -d'|' -f2)
  old_index=$(echo     "$old_inferred" | cut -d'|' -f3)

  local id_file
  id_file=$(identity::path "$identity_name")
  [[ ! -f "$id_file" ]] && return 0

  # Infer new identity context from new name
  local new_inferred new_identity new_type new_index
  new_inferred=$(identity::infer "$new_name")
  if [[ -n "$new_inferred" ]]; then
    new_identity=$(echo "$new_inferred" | cut -d'|' -f1)
    new_type=$(echo     "$new_inferred" | cut -d'|' -f2)
    new_index=$(echo    "$new_inferred" | cut -d'|' -f3)
  else
    # New name doesn't match convention — detach cleanly
    json::identity_remove_peer "$id_file" "$old_name" </dev/null
    return 0
  fi

  # Remove old entry
  json::identity_remove_peer "$id_file" "$old_name" </dev/null

  if [[ "$new_identity" == "$identity_name" ]]; then
    # Same identity — update in place
    json::identity_add_peer "$id_file" "$identity_name" "$new_name" "$new_type" "$new_index" </dev/null
  else
    # Identity changed (e.g. phone-nuno -> phone-helena) — move to new identity file
    local new_id_file
    new_id_file=$(identity::path "$new_identity")
    json::identity_add_peer "$new_id_file" "$new_identity" "$new_name" "$new_type" "$new_index" </dev/null
    # Clean up old identity if empty
    local remaining
    remaining=$(json::identity_peers "$id_file" 2>/dev/null) || true
    if [[ -z "$remaining" ]]; then
      rm -f "$id_file"
    fi
  fi
}


# identity::policy <identity_name>
# Returns the policy name assigned to an identity, or "default".
function identity::policy() {
  local identity_name="${1:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
  [[ ! -f "$id_file" ]] && echo "default" && return 0
  local result
  result=$(json::get "$id_file" "policy" 2>/dev/null) || true
  echo "${result:-default}"
}
 
# identity::set_policy <identity_name> <policy_name>
# Sets the policy on an identity file.
function identity::set_policy() {
  local identity_name="${1:-}" policy_name="${2:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
  if [[ ! -f "$id_file" ]]; then
    log::error "Identity '${identity_name}' not found"
    return 1
  fi
  json::set "$id_file" "policy" "$policy_name"
}
 
# identity::rule_flags <identity_name> <flag>
# Returns a specific rule_flag from the identity file.
# Falls back to the policy's value if not explicitly set on the identity.
function identity::rule_flags() {
  local identity_name="${1:-}" flag="${2:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
 
  local result
  result=$(json::get_nested "$id_file" "rule_flags" "$flag" 2>/dev/null) || true
 
  if [[ -n "$result" ]]; then
    echo "$result"
    return 0
  fi
 
  # Fall back to policy value
  local policy_name
  policy_name=$(identity::policy "$identity_name")
  policy::get "$policy_name" "$flag"
}
 
# identity::set_rule_flag <identity_name> <flag> <value>
# Sets a rule_flag directly on the identity file.
function identity::set_rule_flag() {
  local identity_name="${1:-}" flag="${2:-}" value="${3:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
  if [[ ! -f "$id_file" ]]; then
    log::error "Identity '${identity_name}' not found"
    return 1
  fi
  json::set_nested "$id_file" "rule_flags" "$flag" "$value"
}
 
# identity::reapply_rules <identity_name>
# Reapply the identity rule to all peers in this identity.
# Respects auto_apply flag — if false, does nothing.
function identity::reapply_rules() {
  local identity_name="${1:-}"
 
  # Check auto_apply
  local auto
  auto=$(identity::rule_flags "$identity_name" "auto_apply")
  [[ "$auto" == "false" ]] && return 0
 
  local identity_rule
  identity_rule=$(identity::rule "$identity_name")
  [[ -z "$identity_rule" ]] && return 0
 
  local peers
  peers=$(identity::peers "$identity_name")
  [[ -z "$peers" ]] && return 0
 
  while IFS= read -r peer_name; do
    [[ -z "$peer_name" ]] && continue
    local client_ip
    client_ip=$(peers::get_ip "$peer_name") || continue
    rule::full_restore_peer "$peer_name" "$client_ip"
  done <<< "$peers"
}

# identity::rules <identity_name>
# Returns all rules assigned to an identity, one per line.
# Empty output if no rules assigned.
function identity::rules() {
  local identity_name="${1:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
  [[ ! -f "$id_file" ]] && return 0
  json::identity_rules "$id_file" 2>/dev/null || true
}
 
# identity::has_rule <identity_name> <rule_name>
# Returns 0 if identity has this rule, 1 otherwise.
function identity::has_rule() {
  local identity_name="${1:-}" rule_name="${2:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
  json::identity_has_rule "$id_file" "$rule_name" 2>/dev/null
}
 
# identity::add_rule <identity_name> <rule_name>
# Adds a rule to an identity. Warns if already present (exit 2).
function identity::add_rule() {
  local identity_name="${1:-}" rule_name="${2:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")

  local exit_code=0
  json::identity_add_rule "$id_file" "$identity_name" "$rule_name" 2>/dev/null || exit_code=$?
  return $exit_code
}
 
# identity::remove_rule <identity_name> <rule_name>
# Removes a specific rule from an identity.
function identity::remove_rule() {
  local identity_name="${1:-}" rule_name="${2:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
  json::identity_remove_rule "$id_file" "$rule_name" 2>/dev/null
}
 
# identity::clear_rules <identity_name>
# Removes all rules from an identity.
function identity::clear_rules() {
  local identity_name="${1:-}"
  local id_file
  id_file=$(ctx::identity::path "${identity_name}.identity")
  json::identity_clear_rules "$id_file" 2>/dev/null
}
 
# identity::reapply_rules <identity_name>
# Reapply all identity rules to all peers in this identity.
# Respects auto_apply flag.
function identity::reapply_rules() {
  local identity_name="${1:-}"
 
  local auto
  auto=$(identity::rule_flags "$identity_name" "auto_apply")
  [[ "$auto" == "false" ]] && return 0
 
  local rules
  rules=$(identity::rules "$identity_name")
  [[ -z "$rules" ]] && return 0
 
  local peers
  peers=$(identity::peers "$identity_name")
  [[ -z "$peers" ]] && return 0
 
  while IFS= read -r peer_name; do
    [[ -z "$peer_name" ]] && continue
    local client_ip
    client_ip=$(peers::get_ip "$peer_name") || continue
    rule::full_restore_peer "$peer_name" "$client_ip"
  done <<< "$peers"
}