defmodule Mix.Tasks.Raxol.Memory.Profiler do
  @moduledoc """
  Interactive memory profiler for real-time memory analysis.

  This task provides an interactive interface for profiling memory usage
  in real-time, allowing developers to monitor memory patterns, detect
  hotspots, and analyze memory allocation patterns.

  ## Usage

      mix raxol.memory.profiler
      mix raxol.memory.profiler --mode live
      mix raxol.memory.profiler --target Terminal.Buffer
      mix raxol.memory.profiler --format dashboard

  ## Options

    * `--mode` - Profiling mode (live, snapshot, trace) (default: live)
    * `--target` - Target module to profile (default: all)
    * `--format` - Output format (dashboard, text, json) (default: dashboard)
    * `--interval` - Update interval in seconds for live mode (default: 1)
    * `--duration` - Duration for profiling in seconds (default: 60)
    * `--output` - Output file for results
    * `--threshold` - Memory allocation threshold in bytes (default: 1024)

  ## Profiling Modes

  ### live
  Real-time memory monitoring with interactive dashboard.
  Shows live memory usage, allocation patterns, and GC activity.

  ### snapshot
  Take memory snapshots at specific intervals and compare them.
  Useful for detecting memory leaks and growth patterns.

  ### trace
  Trace memory allocations and deallocations in detail.
  Provides detailed allocation traces and call stacks.

  ## Output Formats

  ### dashboard
  Interactive terminal dashboard with real-time updates.

  ### text
  Plain text output suitable for logging and analysis.

  ### json
  JSON format for programmatic consumption.

  ## Controls (Dashboard Mode)

  - `q` - Quit
  - `r` - Reset statistics
  - `s` - Take snapshot
  - `g` - Force garbage collection
  - `p` - Pause/Resume updates
  - `↑/↓` - Navigate through processes
  - `Tab` - Switch between views (Memory/Processes/GC)
  """

  use Mix.Task

  alias Raxol.Core.Runtime.Log
  alias Raxol.Utils.MemoryFormatter

  @shortdoc "Interactive memory profiler for real-time analysis"

  @spec run(list()) :: no_return()
  def run(args) do
    {opts, _, _} =
      OptionParser.parse(args,
        switches: [
          mode: :string,
          target: :string,
          format: :string,
          interval: :integer,
          duration: :integer,
          output: :string,
          threshold: :integer,
          help: :boolean
        ],
        aliases: [
          m: :mode,
          t: :target,
          f: :format,
          i: :interval,
          d: :duration,
          o: :output,
          h: :help
        ]
      )

    if opts[:help] do
      print_help()
      System.halt(0)
    end

    _ = Application.ensure_all_started(:raxol)

    config = build_config(opts)

    case config.mode do
      "live" ->
        run_live_profiler(config)

      "snapshot" ->
        run_snapshot_profiler(config)

      "trace" ->
        run_trace_profiler(config)

      _ ->
        Mix.shell().error("Unknown mode: #{config.mode}")
        System.halt(1)
    end
  end

  defp build_config(opts) do
    %{
      mode: Keyword.get(opts, :mode, "live"),
      target: Keyword.get(opts, :target, "all"),
      format: Keyword.get(opts, :format, "dashboard"),
      interval: Keyword.get(opts, :interval, 1),
      duration: Keyword.get(opts, :duration, 60),
      output: Keyword.get(opts, :output),
      threshold: Keyword.get(opts, :threshold, 1024)
    }
  end

  defp run_live_profiler(config) do
    Mix.shell().info("Starting live memory profiler...")
    Mix.shell().info("Mode: #{config.mode}, Format: #{config.format}")

    Mix.shell().info(
      "Interval: #{config.interval}s, Duration: #{config.duration}s"
    )

    case config.format do
      "dashboard" ->
        run_interactive_dashboard(config)

      "text" ->
        run_text_profiler(config)

      "json" ->
        run_json_profiler(config)

      _ ->
        Mix.shell().error("Unknown format: #{config.format}")
        System.halt(1)
    end
  end

  defp run_interactive_dashboard(config) do
    # Initialize dashboard state
    dashboard_state = %{
      running: true,
      paused: false,
      # :memory, :processes, :gc
      view: :memory,
      snapshots: [],
      start_time: System.monotonic_time(:millisecond),
      config: config
    }

    # Start background monitoring
    monitoring_pid = spawn_link(fn -> monitor_memory_background(config) end)

    # Run interactive loop
    try do
      dashboard_loop(dashboard_state, monitoring_pid)
    after
      Process.exit(monitoring_pid, :normal)
    end
  end

  defp dashboard_loop(state, monitoring_pid) do
    if state.running do
      # Clear screen and draw dashboard
      # Clear screen and move cursor to top
      IO.write("\e[2J\e[H")
      draw_dashboard(state)

      # Handle input with timeout
      case get_input_with_timeout(state.config.interval * 1000) do
        {:input, key} ->
          new_state = handle_dashboard_input(key, state, monitoring_pid)
          dashboard_loop(new_state, monitoring_pid)

        :timeout ->
          handle_dashboard_timeout(state, monitoring_pid)
      end
    end
  end

  defp draw_dashboard(state) do
    # Header
    elapsed = (System.monotonic_time(:millisecond) - state.start_time) / 1000
    Log.info("Raxol Memory Profiler - Live Dashboard")
    Log.info("#{String.duplicate("=", 80)}")

    Log.info(
      "Elapsed: #{Float.round(elapsed, 1)}s | View: #{state.view} | #{if state.paused, do: "PAUSED", else: "RUNNING"}"
    )

    Log.info("#{String.duplicate("-", 80)}")

    # Current memory info
    memory_info = get_current_memory_info()
    draw_memory_overview(memory_info)

    # View-specific content
    case state.view do
      :memory -> draw_memory_view(memory_info)
      :processes -> draw_processes_view()
      :gc -> draw_gc_view()
    end

    # Footer with controls
    Log.info("#{String.duplicate("-", 80)}")

    Log.info(
      "Controls: [q]uit | [r]eset | [s]napshot | [g]c | [p]ause | Tab:switch view"
    )
  end

  defp draw_memory_overview(memory_info) do
    Log.info("Memory Overview:")
    Log.info("  Total:     #{format_memory(memory_info.total)}")

    Log.info(
      "  Processes: #{format_memory(memory_info.processes)} (#{percentage(memory_info.processes, memory_info.total)}%)"
    )

    Log.info(
      "  System:    #{format_memory(memory_info.system)} (#{percentage(memory_info.system, memory_info.total)}%)"
    )

    Log.info(
      "  Binary:    #{format_memory(memory_info.binary)} (#{percentage(memory_info.binary, memory_info.total)}%)"
    )

    Log.info(
      "  ETS:       #{format_memory(memory_info.ets)} (#{percentage(memory_info.ets, memory_info.total)}%)"
    )

    Log.info("")
  end

  defp draw_memory_view(memory_info) do
    Log.info("Detailed Memory Breakdown:")

    # Memory by type
    memory_types = [
      {"Atom", memory_info.atom},
      {"Binary", memory_info.binary},
      {"Code", Map.get(memory_info, :code, 0)},
      {"ETS", memory_info.ets}
    ]

    Enum.each(memory_types, fn {type, bytes} ->
      bar = create_progress_bar(bytes, memory_info.total, 40)

      Log.info(
        "  #{String.pad_trailing(type, 12)} #{format_memory(bytes)} #{bar}"
      )
    end)

    Log.info("")

    # Recent allocations (simulated)
    Log.info("Recent Large Allocations:")

    recent_allocations = [
      {"Buffer.create/2", 2_048_576, "2.0MB"},
      {"AnsiParser.parse/1", 524_288, "512KB"},
      {"String operations", 131_072, "128KB"}
    ]

    Enum.each(recent_allocations, fn {operation, _bytes, size} ->
      Log.info("  #{String.pad_trailing(operation, 25)} #{size}")
    end)
  end

  defp draw_processes_view do
    Log.info("Top Memory-Consuming Processes:")

    Log.info(
      String.pad_trailing("PID", 8) <>
        String.pad_trailing("Name", 25) <>
        String.pad_trailing("Memory", 12) <> "Heap"
    )

    top_processes()
    |> Enum.each(&print_process_row/1)
  end

  defp top_processes do
    Process.list()
    |> Enum.map(&extract_process_info/1)
    |> Enum.reject(&is_nil/1)
    |> Enum.sort_by(& &1.memory, :desc)
    |> Enum.take(10)
  end

  defp extract_process_info(pid) do
    case Process.info(pid, [:memory, :registered_name, :current_function]) do
      [memory: mem, registered_name: name, current_function: func] ->
        %{pid: pid, memory: mem, name: name || func, registered_name: name}

      _ ->
        nil
    end
  end

  defp print_process_row(proc) do
    pid_str = inspect(proc.pid) |> String.pad_trailing(8)
    name_str = format_process_name(proc.name) |> String.pad_trailing(25)
    memory_str = format_memory(proc.memory) |> String.pad_trailing(12)
    Log.info("  #{pid_str}#{name_str}#{memory_str}")
  end

  defp draw_gc_view do
    Log.info("Garbage Collection Statistics:")

    # Get GC stats (simplified)
    gc_info = :erlang.statistics(:garbage_collection)
    {gc_count, words_reclaimed, _} = gc_info

    Log.info("  Collections: #{gc_count}")
    Log.info("  Words reclaimed: #{words_reclaimed}")

    Log.info(
      "  Average per collection: #{div(words_reclaimed, max(gc_count, 1))} words"
    )

    Log.info("")

    # Recent GC activity (simulated timeline)
    Log.info("Recent GC Activity:")

    timeline = [
      {"-5s", "Minor GC", "2ms", "128KB reclaimed"},
      {"-12s", "Major GC", "8ms", "2.4MB reclaimed"},
      {"-18s", "Minor GC", "1ms", "64KB reclaimed"},
      {"-25s", "Minor GC", "2ms", "256KB reclaimed"}
    ]

    Enum.each(timeline, fn {time, type, duration, reclaimed} ->
      Log.info(
        "  #{String.pad_trailing(time, 6)} #{String.pad_trailing(type, 12)} #{String.pad_trailing(duration, 8)} #{reclaimed}"
      )
    end)
  end

  defp handle_dashboard_input(key, state, _monitoring_pid) do
    case key do
      "q" -> %{state | running: false}
      "r" -> reset_statistics(state)
      "s" -> take_snapshot(state)
      "g" -> force_garbage_collection(state)
      "p" -> %{state | paused: not state.paused}
      # Tab key
      "\t" -> cycle_view(state)
      _ -> state
    end
  end

  defp reset_statistics(state) do
    Mix.shell().info("Statistics reset")
    %{state | start_time: System.monotonic_time(:millisecond)}
  end

  defp take_snapshot(state) do
    snapshot = %{
      timestamp: System.monotonic_time(:millisecond),
      memory: get_current_memory_info()
    }

    Mix.shell().info("Snapshot taken")
    %{state | snapshots: [snapshot | state.snapshots]}
  end

  defp force_garbage_collection(state) do
    :erlang.garbage_collect()
    Mix.shell().info("Garbage collection forced")
    state
  end

  defp cycle_view(state) do
    next_view =
      case state.view do
        :memory -> :processes
        :processes -> :gc
        :gc -> :memory
      end

    %{state | view: next_view}
  end

  defp update_dashboard_data(state) do
    # This would update any cached data for the dashboard
    state
  end

  defp get_input_with_timeout(timeout) do
    # Simplified input handling - in real implementation would use proper terminal input
    receive do
      {:input, char} -> {:input, char}
    after
      timeout -> :timeout
    end
  end

  defp run_text_profiler(config) do
    Mix.shell().info("Starting text memory profiler...")
    start_time = System.monotonic_time(:millisecond)

    profiler_loop = fn loop_fn, iteration ->
      current_time = System.monotonic_time(:millisecond)
      elapsed = (current_time - start_time) / 1000

      if elapsed < config.duration do
        memory_info = get_current_memory_info()

        Mix.shell().info(
          "\n--- Memory Report (#{Float.round(elapsed, 1)}s) ---"
        )

        print_text_memory_report(memory_info)

        Process.sleep(config.interval * 1000)
        loop_fn.(loop_fn, iteration + 1)
      else
        Mix.shell().info("Profiling completed after #{config.duration}s")
      end
    end

    profiler_loop.(profiler_loop, 1)
  end

  defp run_json_profiler(config) do
    Mix.shell().info("Starting JSON memory profiler...")
    start_time = System.monotonic_time(:millisecond)
    reports = []

    profiler_loop = fn loop_fn, iteration, acc_reports ->
      current_time = System.monotonic_time(:millisecond)
      elapsed = (current_time - start_time) / 1000

      case elapsed < config.duration do
        true ->
          memory_info = get_current_memory_info()

          report = %{
            timestamp: current_time,
            elapsed: elapsed,
            iteration: iteration,
            memory: memory_info
          }

          json_output = Jason.encode!(report, pretty: true)
          Log.info(json_output)

          Process.sleep(config.interval * 1000)
          loop_fn.(loop_fn, iteration + 1, [report | acc_reports])

        false ->
          save_profiler_final_report(config, start_time, acc_reports)
      end
    end

    profiler_loop.(profiler_loop, 1, reports)
  end

  defp run_snapshot_profiler(config) do
    Mix.shell().info("Starting snapshot memory profiler...")
    snapshots = take_memory_snapshots(config)
    analyze_snapshots(snapshots, config)
  end

  defp run_trace_profiler(config) do
    Mix.shell().info("Starting trace memory profiler...")

    Mix.shell().info(
      "Note: Detailed allocation tracing would require additional BEAM tools"
    )

    # This would use tools like :recon_trace or custom tracing
    # For now, provide a simplified version
    trace_large_allocations(config)
  end

  defp take_memory_snapshots(config) do
    Mix.shell().info(
      "Taking memory snapshots every #{config.interval}s for #{config.duration}s..."
    )

    start_time = System.monotonic_time(:millisecond)
    snapshot_count = div(config.duration, config.interval)

    Enum.map(1..snapshot_count, fn i ->
      current_time = System.monotonic_time(:millisecond)
      elapsed = (current_time - start_time) / 1000

      snapshot = %{
        index: i,
        timestamp: current_time,
        elapsed: elapsed,
        memory: get_current_memory_info()
      }

      Mix.shell().info(
        "Snapshot #{i}/#{snapshot_count} - Memory: #{format_memory(snapshot.memory.total)}"
      )

      if i < snapshot_count do
        Process.sleep(config.interval * 1000)
      end

      snapshot
    end)
  end

  defp analyze_snapshots(snapshots, config) do
    Mix.shell().info("\nAnalyzing #{length(snapshots)} snapshots...")

    if length(snapshots) >= 2 do
      first = List.first(snapshots)
      last = List.last(snapshots)

      growth = last.memory.total - first.memory.total
      # bytes per hour
      growth_rate = growth / (last.elapsed - first.elapsed) * 3600

      Mix.shell().info("Memory Growth Analysis:")
      Mix.shell().info("  Initial: #{format_memory(first.memory.total)}")
      Mix.shell().info("  Final: #{format_memory(last.memory.total)}")
      Mix.shell().info("  Growth: #{format_memory(growth)}")

      Mix.shell().info(
        "  Growth rate: #{format_memory(trunc(growth_rate))}/hour"
      )

      # Save detailed analysis
      if config.output do
        analysis = %{
          config: config,
          snapshots: snapshots,
          analysis: %{
            initial_memory: first.memory.total,
            final_memory: last.memory.total,
            total_growth: growth,
            growth_rate_per_hour: growth_rate
          }
        }

        File.write!(config.output, Jason.encode!(analysis, pretty: true))
        Mix.shell().info("Analysis saved to: #{config.output}")
      end
    else
      Mix.shell().info("Not enough snapshots for analysis")
    end
  end

  defp trace_large_allocations(config) do
    Mix.shell().info(
      "Monitoring large allocations (threshold: #{format_memory(config.threshold)})..."
    )

    # This is a simplified simulation of allocation tracing
    start_time = System.monotonic_time(:millisecond)

    trace_loop = fn loop_fn ->
      current_time = System.monotonic_time(:millisecond)
      elapsed = (current_time - start_time) / 1000

      if elapsed < config.duration do
        # Simulate detecting large allocations
        simulate_allocation_detection(config)
        # Check more frequently for allocations
        Process.sleep(100)
        loop_fn.(loop_fn)
      end
    end

    trace_loop.(trace_loop)
  end

  defp simulate_allocation_detection(config) do
    # This would normally hook into BEAM allocation events
    # For simulation, randomly report large allocations

    # 5% chance per check
    if :rand.uniform(100) < 5 do
      size = :rand.uniform(10) * config.threshold

      operation =
        Enum.random([
          "Buffer.create",
          "String.duplicate",
          "List.duplicate",
          "Binary.copy"
        ])

      Mix.shell().info(
        "Large allocation detected: #{operation} - #{format_memory(size)}"
      )
    end
  end

  # Utility functions
  defp monitor_memory_background(config) do
    # Background memory monitoring for the dashboard
    receive do
      :stop -> :ok
    after
      config.interval * 1000 ->
        # Could send memory updates to dashboard process
        monitor_memory_background(config)
    end
  end

  defp get_current_memory_info do
    %{
      total: :erlang.memory(:total),
      processes: :erlang.memory(:processes),
      system: :erlang.memory(:system),
      atom: :erlang.memory(:atom),
      binary: :erlang.memory(:binary),
      ets: :erlang.memory(:ets)
    }
  end

  defp print_text_memory_report(memory_info) do
    Mix.shell().info("Total Memory: #{format_memory(memory_info.total)}")

    Mix.shell().info(
      "Processes: #{format_memory(memory_info.processes)} (#{percentage(memory_info.processes, memory_info.total)}%)"
    )

    Mix.shell().info(
      "System: #{format_memory(memory_info.system)} (#{percentage(memory_info.system, memory_info.total)}%)"
    )

    Mix.shell().info(
      "Binary: #{format_memory(memory_info.binary)} (#{percentage(memory_info.binary, memory_info.total)}%)"
    )

    Mix.shell().info(
      "ETS: #{format_memory(memory_info.ets)} (#{percentage(memory_info.ets, memory_info.total)}%)"
    )
  end

  defp format_memory(bytes), do: MemoryFormatter.format_memory(bytes)

  defp format_process_name(name) when is_atom(name), do: Atom.to_string(name)
  defp format_process_name({mod, func, arity}), do: "#{mod}.#{func}/#{arity}"
  defp format_process_name(other), do: inspect(other)

  defp percentage(part, whole) when whole > 0,
    do: Float.round(part / whole * 100, 1)

  defp percentage(_, _), do: 0.0

  defp create_progress_bar(value, max, width) do
    filled = trunc(value / max * width)
    empty = width - filled
    "[#{String.duplicate("=", filled)}#{String.duplicate(" ", empty)}]"
  end

  defp print_help do
    Mix.shell().info(@moduledoc)
  end

  defp handle_dashboard_timeout(state, monitoring_pid) do
    case state.paused do
      true ->
        dashboard_loop(state, monitoring_pid)

      false ->
        updated_state = update_dashboard_data(state)
        dashboard_loop(updated_state, monitoring_pid)
    end
  end

  defp save_profiler_final_report(config, start_time, acc_reports) do
    case config.output do
      nil ->
        :ok

      output_file ->
        final_report = %{
          config: config,
          start_time: start_time,
          reports: Enum.reverse(acc_reports)
        }

        File.write!(output_file, Jason.encode!(final_report, pretty: true))
        Mix.shell().info("Results saved to: #{output_file}")
    end
  end
end
