defmodule ToweropsWeb.DeviceLive.Form do
  @moduledoc false
  use ToweropsWeb, :live_view

  alias Towerops.Agents
  alias Towerops.Devices
  alias Towerops.Devices.Device, as: DeviceSchema
  alias Towerops.Sites
  alias Towerops.Snmp
  alias Towerops.Workers.DiscoveryWorker

  @impl true
  def mount(params, _session, socket) do
    organization = socket.assigns.current_organization
    sites = Sites.list_organization_sites(organization.id)
    agents = Agents.list_organization_agent_tokens(organization.id)

    # Redirect to sites page if no sites exist
    if Enum.empty?(sites) do
      {:ok,
       socket
       |> put_flash(:info, "Please create a site before adding a device.")
       |> push_navigate(to: ~p"/sites/new")}
    else
      {:ok,
       socket
       |> assign(:organization, organization)
       |> assign(:available_sites, sites)
       |> assign(:available_agents, agents)
       |> assign(:preselected_site_id, params["site_id"])
       |> assign(:snmp_test_result, nil)}
    end
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :new, _params) do
    equipment_attrs = %{
      monitoring_enabled: true,
      check_interval_seconds: 300,
      snmp_enabled: true,
      snmp_version: "2c",
      snmp_port: 161
    }

    equipment_attrs =
      cond do
        # Use preselected site from URL params if available
        socket.assigns.preselected_site_id ->
          Map.put(equipment_attrs, :site_id, socket.assigns.preselected_site_id)

        # Auto-select if there's only one site
        length(socket.assigns.available_sites) == 1 ->
          site = List.first(socket.assigns.available_sites)
          Map.put(equipment_attrs, :site_id, site.id)

        # Otherwise, leave it empty for user to select
        true ->
          equipment_attrs
      end

    changeset = Devices.change_device(%DeviceSchema{}, equipment_attrs)

    socket
    |> assign(:page_title, "New Device")
    |> assign(:device, %DeviceSchema{})
    |> assign(:form, to_form(changeset))
    |> assign(:monitoring_mode, "snmp_and_icmp")
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    device = Devices.get_device!(id)

    # Preload necessary associations for agent resolution
    device = Towerops.Repo.preload(device, site: [organization: :default_agent_token])

    # Get current agent assignment if any
    current_assignment = Agents.get_device_assignment(device.id)

    # Get the effective agent and where it comes from
    {effective_agent_id, agent_source} = Agents.get_effective_agent_token_with_source(device)

    # For the form, we only want the device-specific assignment
    agent_token_id =
      if current_assignment do
        current_assignment.agent_token_id
      end

    # Get agent name for display
    effective_agent_name =
      if effective_agent_id do
        agent = Enum.find(socket.assigns.available_agents, &(&1.id == effective_agent_id))
        agent && agent.name
      end

    # Add agent_token_id to the changeset data
    device_with_agent = Map.put(device, :agent_token_id, agent_token_id)
    changeset = Devices.change_device(device, %{})

    # Get effective SNMP configuration and source
    snmp_config = Devices.get_snmp_config(device)

    # Determine monitoring mode based on current snmp_enabled value
    monitoring_mode = if device.snmp_enabled, do: "snmp_and_icmp", else: "icmp_only"

    socket
    |> assign(:page_title, "Edit Device")
    |> assign(:device, device_with_agent)
    |> assign(:form, to_form(changeset))
    |> assign(:agent_source, agent_source)
    |> assign(:effective_agent_name, effective_agent_name)
    |> assign(:snmp_config_source, snmp_config.source)
    |> assign(:effective_snmp_version, snmp_config.version)
    |> assign(:effective_snmp_community, snmp_config.community || "(not set)")
    |> assign(:monitoring_mode, monitoring_mode)
  end

  @impl true
  def handle_event("switch_monitoring_mode", %{"mode" => mode}, socket) do
    # Update snmp_enabled based on the selected mode
    snmp_enabled = mode == "snmp_and_icmp"

    # Get current form params
    current_params = socket.assigns.form.params

    # Update params with new snmp_enabled value
    updated_params = Map.put(current_params, "snmp_enabled", snmp_enabled)

    # Create new changeset with updated params
    changeset =
      socket.assigns.device
      |> Devices.change_device(updated_params)
      |> Map.put(:action, :validate)

    socket
    |> assign(:monitoring_mode, mode)
    |> assign(:form, to_form(changeset))
    |> then(&{:noreply, &1})
  end

  @impl true
  def handle_event("validate", %{"device" => device_params}, socket) do
    changeset =
      socket.assigns.device
      |> Devices.change_device(device_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, :form, to_form(changeset))}
  end

  @impl true
  def handle_event("save", %{"device" => device_params}, socket) do
    save_device(socket, socket.assigns.live_action, device_params)
  end

  @impl true
  def handle_event("delete", _params, socket) do
    case Devices.delete_device(socket.assigns.device) do
      {:ok, _} ->
        {:noreply,
         socket
         |> put_flash(:info, "Device deleted successfully")
         |> push_navigate(to: ~p"/devices")}

      {:error, _} ->
        {:noreply, put_flash(socket, :error, "Unable to delete device")}
    end
  end

  @impl true
  def handle_event("test_snmp", _params, socket) do
    # Check if an agent is assigned
    agent_token_id = socket.assigns.form.params["agent_token_id"]

    result =
      if agent_token_id && agent_token_id != "" do
        # Agent is configured - can't test directly from web server
        %{
          success: true,
          message: "SNMP configuration saved. Connection will be tested by the assigned agent during next polling cycle."
        }
      else
        # No agent configured - test directly from web server
        snmp_config = extract_snmp_config(socket.assigns.form, socket.assigns)
        test_snmp_connection(snmp_config)
      end

    {:noreply, assign(socket, :snmp_test_result, result)}
  end

  @impl true
  def handle_event("trigger_discovery", _params, socket) do
    device = socket.assigns.device

    if device.snmp_enabled do
      # Enqueue discovery job
      enqueue_discovery(device.id)

      # Navigate to device show page where live updates will appear
      {:noreply,
       socket
       |> put_flash(:info, "Discovery started...")
       |> push_navigate(to: ~p"/devices/#{device.id}")}
    else
      {:noreply, put_flash(socket, :error, "SNMP is not enabled for this device")}
    end
  end

  defp save_device(socket, :new, device_params) do
    # Extract agent_token_id before creating device
    agent_token_id = Map.get(device_params, "agent_token_id")
    device_params = Map.delete(device_params, "agent_token_id")

    # Add snmp_enabled based on monitoring mode
    snmp_enabled = socket.assigns.monitoring_mode == "snmp_and_icmp"
    device_params = Map.put(device_params, "snmp_enabled", snmp_enabled)

    case Devices.create_device(device_params) do
      {:ok, device} ->
        # Handle agent assignment after device creation
        handle_agent_assignment(device.id, agent_token_id)

        flash_message = handle_device_creation(device)

        # Reset form for next device while staying on the add page
        fresh_attrs = %{
          monitoring_enabled: true,
          check_interval_seconds: 300,
          snmp_enabled: snmp_enabled,
          snmp_version: "2c",
          snmp_port: 161,
          # Keep the same site selected
          site_id: device.site_id
        }

        fresh_changeset = Devices.change_device(%DeviceSchema{}, fresh_attrs)

        {:noreply,
         socket
         |> put_flash(:info, flash_message)
         |> assign(:form, to_form(fresh_changeset))
         |> assign(:snmp_test_result, nil)
         |> push_event("scroll_to_top", %{})}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :form, to_form(changeset))}
    end
  end

  defp save_device(socket, :edit, device_params) do
    old_device = socket.assigns.device

    # Extract agent_token_id before updating device
    agent_token_id = Map.get(device_params, "agent_token_id")
    device_params = Map.delete(device_params, "agent_token_id")

    # Add snmp_enabled based on monitoring mode
    snmp_enabled = socket.assigns.monitoring_mode == "snmp_and_icmp"
    device_params = Map.put(device_params, "snmp_enabled", snmp_enabled)

    case Devices.update_device(old_device, device_params) do
      {:ok, device} ->
        # device update
        handle_agent_assignment(device.id, agent_token_id)

        flash_message = handle_device_update(old_device, device)

        {:noreply,
         socket
         |> put_flash(:info, flash_message)
         |> push_navigate(to: ~p"/devices/#{device.id}")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :form, to_form(changeset))}
    end
  end

  defp handle_device_creation(device) do
    if device.snmp_enabled do
      enqueue_discovery(device.id)
      "Device created successfully. SNMP discovery started in background."
    else
      "Device created successfully"
    end
  end

  defp handle_device_update(old_device, device) do
    if should_trigger_snmp_discovery?(old_device, device) do
      enqueue_discovery(device.id)
      "Device updated successfully. SNMP discovery started in background."
    else
      "Device updated successfully"
    end
  end

  # Enqueue discovery job - safe to call in test environment
  defp enqueue_discovery(device_id) do
    if Application.get_env(:towerops, :env) == :test do
      # In test, run synchronously
      Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end)
    else
      # In dev/prod, enqueue to Exq
      {:ok, _job} = Exq.enqueue(Exq, "discovery", DiscoveryWorker, [device_id])
    end
  end

  defp should_trigger_snmp_discovery?(old_device, device) do
    device.snmp_enabled and
      (!old_device.snmp_enabled or
         snmp_config_changed?(old_device, device))
  end

  defp snmp_config_changed?(old_device, device) do
    device.snmp_community != old_device.snmp_community or
      device.snmp_version != old_device.snmp_version or
      device.snmp_port != old_device.snmp_port or
      device.ip_address != old_device.ip_address
  end

  @spec extract_snmp_config(Phoenix.HTML.Form.t(), map()) :: %{
          ip_address: String.t() | nil,
          snmp_community: String.t() | nil,
          snmp_version: String.t() | nil,
          snmp_port: integer()
        }
  defp extract_snmp_config(form, assigns) do
    form_data = form.data
    params = form.params

    device_community = params["snmp_community"] || form_data.snmp_community

    # If device community is nil or empty, resolve from site/org hierarchy
    effective_community =
      if is_nil(device_community) or device_community == "" do
        resolve_inherited_community(assigns)
      else
        device_community
      end

    %{
      ip_address: params["ip_address"] || form_data.ip_address,
      snmp_community: effective_community,
      snmp_version: params["snmp_version"] || form_data.snmp_version,
      snmp_port: normalize_port(params["snmp_port"] || form_data.snmp_port || 161)
    }
  end

  defp resolve_inherited_community(assigns) do
    cond do
      # For new devices, check preselected site
      assigns[:preselected_site_id] ->
        site = Enum.find(assigns.available_sites, &(&1.id == assigns.preselected_site_id))
        site && site.snmp_community

      # For edit mode, use effective SNMP community
      assigns[:effective_snmp_community] && assigns.effective_snmp_community != "(not set)" ->
        assigns.effective_snmp_community

      # Fallback: check if there's only one site
      length(assigns[:available_sites] || []) == 1 ->
        site = List.first(assigns.available_sites)
        site && site.snmp_community

      true ->
        nil
    end
  end

  @spec normalize_port(integer() | String.t() | any()) :: integer()
  defp normalize_port(port) when is_integer(port) and port >= 1 and port <= 65_535, do: port
  defp normalize_port(port) when is_integer(port), do: 161

  defp normalize_port(port) when is_binary(port) do
    case Integer.parse(port) do
      {parsed_port, ""} when parsed_port >= 1 and parsed_port <= 65_535 -> parsed_port
      _ -> 161
    end
  end

  defp normalize_port(_), do: 161

  @spec test_snmp_connection(%{
          ip_address: String.t() | nil,
          snmp_community: String.t() | nil,
          snmp_version: String.t() | nil,
          snmp_port: integer()
        }) :: %{success: boolean(), message: String.t()}
  defp test_snmp_connection(config) do
    # Validate IP address before testing
    case validate_test_snmp_input(config) do
      :ok ->
        case Snmp.test_connection(
               config.ip_address,
               config.snmp_community,
               config.snmp_version,
               config.snmp_port
             ) do
          {:ok, message} ->
            %{success: true, message: message}

          {:error, reason} ->
            %{success: false, message: "Connection failed: #{inspect(reason)}"}
        end

      {:error, message} ->
        %{success: false, message: message}
    end
  end

  @spec validate_test_snmp_input(%{
          ip_address: String.t() | nil,
          snmp_community: String.t() | nil,
          snmp_version: String.t() | nil,
          snmp_port: integer()
        }) :: :ok | {:error, String.t()}
  defp validate_test_snmp_input(%{ip_address: nil}) do
    {:error, "IP address is required"}
  end

  defp validate_test_snmp_input(%{snmp_community: nil}) do
    {:error, "SNMP community string is required. Set one at the device, site, or organization level."}
  end

  defp validate_test_snmp_input(%{snmp_community: ""}) do
    {:error, "SNMP community string is required. Set one at the device, site, or organization level."}
  end

  defp validate_test_snmp_input(%{ip_address: ip}) do
    # Validate IP address format using Erlang's inet module
    case ip |> String.to_charlist() |> :inet.parse_address() do
      {:ok, _} -> :ok
      {:error, _} -> {:error, "Invalid IP address format"}
    end
  end

  defp handle_agent_assignment(device_id, agent_token_id) when is_binary(agent_token_id) do
    # Only assign if agent_token_id is not empty
    if agent_token_id == "" do
      Agents.update_device_assignment(device_id, nil)
    else
      Agents.update_device_assignment(device_id, agent_token_id)
    end
  end

  defp handle_agent_assignment(device_id, nil) do
    # No agent selected, remove any assignment
    Agents.update_device_assignment(device_id, nil)
  end

  defp handle_agent_assignment(_device_id, _), do: :ok
end
