#!/usr/bin/env ruby

require 'json'
require 'net/http'
require 'uri'
require 'openssl'

class BunkerWebApiError < StandardError; end
class HestiaBunkerWebApi
  # Override puts to accumulate logs into @extra_info
  def puts(*args)
    @extra_info ||= ""
    @extra_info << args.join("\n") << "\n"
  end

  # Accessor for @extra_info
  def extra_info
    @extra_info || ""
  end
end

# Hook to wrap methods of HestiaBunkerWebApi to reset @extra_info at start
class Module
  alias_method :orig_method_added, :method_added
  def method_added(name)
    orig_method_added(name)
    # Skip wrapping for the overridden puts method
    return if name == :puts
    if self.name == 'HestiaBunkerWebApi'
      @__wrapping ||= false
      return if @__wrapping
      @__wrapping = true
      original = instance_method(name)
      define_method(name) do |*args, &block|
        @extra_info = ""
        original.bind(self).call(*args, &block)
      end
      @__wrapping = false
    end
  end
end
class HestiaBunkerWebApi
  # Retrieve API username and password from /etc/bunkerweb/api.env if available
  def get_api_user_password
    env_path = "/etc/bunkerweb/api.env"
    return nil unless File.file?(env_path)

    username = nil
    password = nil

    File.foreach(env_path) do |line|
      line.strip!
      next if line.empty? || line.start_with?('#')
      key, value = line.split('=', 2)
      next unless key && value
      case key
      when 'API_USERNAME'
        username = value
      when 'API_PASSWORD'
        password = value
      end
    end

    if username && password
      [username, password]
    else
      nil
    end
  end

  def initialize(api_url, username = nil, password = nil)
    @api_base = api_url
    if username.nil?
      result = get_api_user_password
      if result.nil?
        raise BunkerWebApiError.new("Authentication error: no username or password")
      else
        @username = result[0]
        @password = result[1]
      end
    else
      @username = username
      @password = password
    end
    @token = nil
    @extra_info = ""

    # Authenticate and get token
    authenticate!

    puts "[INFO] Successfully authenticated with BunkerWeb API"
  end

  def authenticate!
    uri = URI(@api_base)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = (uri.scheme == "https")

    # Try both Basic Auth and JSON body with credentials
    request = Net::HTTP::Post.new("/auth")
    request.content_type = "application/json"
    request.body = { username: @username, password: @password }.to_json

    response = http.request(request)

    if response.code != '200'
      raise BunkerWebApiError.new("Authentication failed: #{response.code} - #{response.message}")
    end

    body = JSON.parse(response.body)

    unless body['token']
      raise BunkerWebApiError.new("Authentication succeeded but no token received")
    end

    @token = body['token']
  rescue => e
    raise BunkerWebApiError.new("Authentication error: #{e.message}")
  end

  def api_call(method, path, headers = {}, body = nil)
    uri = URI(@api_base + path)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = (uri.scheme == "https")

    request = case method
              when "GET"
                Net::HTTP::Get.new(uri.path)
              when "POST"
                req = Net::HTTP::Post.new(uri.path)
                req.body = body if body
                req
              when "PATCH"
                req = Net::HTTP::Patch.new(uri.path)
                req.body = body if body
                req
              when "DELETE"
                Net::HTTP::Delete.new(uri.path)
              else
                raise BunkerWebApiError.new("Unsupported HTTP method: #{method}")
              end

    # Add Authorization header with token for most operations (except /auth)
    unless path == "/auth"
      headers["Authorization"] = "Bearer #{@token}" if @token
    end

    # Add Content-Type if not already set and we have a body
    if body && !headers.key?("Content-Type")
      headers["Content-Type"] = "application/json"
    end

    headers.each { |k, v| request[k] = v }

    response = http.request(request)
    parsed_body = begin
      JSON.parse(response.body)
    rescue => e
      nil
    end

    { status: response.code.to_i, body: parsed_body || {}, raw_body: response.body }
  rescue => e
    raise BunkerWebApiError.new("API call to #{path} failed: #{e.message}")
  end

  # === Service Operations ===

  def create_service(service_name, options = {})
    @extra_info = ""
    # Create a new service with the given configuration.
    #
    # Args:
    #   service_name (String): The domain name for this service (server_name)
    #   options (Hash): Service configuration including:
    #     - USE_TEMPLATE (default: "high")
    #     - USE_SSL (default: "no") - if not "no", you need CERTIFICATE and KEY paths
    #     - REVERSE_PROXY_HOST (optional)
    #     - REVERSE_PROXY_URL (optional, default: "~ ^/(.*)$")
    #     - Additional options like:
    #       - USE_REVERSE_PROXY (default: "yes" if REVERSE_PROXY_HOST is set)
    #       - USE_REAL_IP, REAL_IP_FROM
    #       - USE_MODSECURITY, USE_ANTIBOT
    #       - LISTEN_HTTP_PORT, LISTEN_HTTPS_PORT
    #     - CERTIFICATE_FILE_PATH (if USE_SSL != "no")
    #     - KEY_FILE_PATH (if USE_SSL != "no")
    # Returns: Hash with creation response

    variables = {
      "USE_TEMPLATE" => options[:use_template] || "high",
      "USE_REVERSE_PROXY" => options[:reverse_proxy_host].nil? ? "no" : "yes",
    }

    # SSL configuration - only if USE_SSL != "no"
    ssl_enabled = options[:ssl] && options[:ssl] != "no"
    variables["USE_SSL"] = ssl_enabled ? "yes" : "no"

    if ssl_enabled
      unless options[:certificate_path] && options[:key_path]
        raise BunkerWebApiError.new("Certificate and Key paths are required when USE_SSL is enabled")
      end

      # Set certificate paths in variables
      variables["SSL_CERTIFICATE_FILE_PATH"] = options[:certificate_path]
      variables["SSL_KEY_FILE_PATH"] = options[:key_path]

      variables["LISTEN_HTTPS_PORT"] = (options[:https_port] || "443").to_s
      variables["USE_REVERSE_PROXY_SSL"] = options[:reverse_proxy_ssl] || "yes"
    else
      # No SSL - HTTP only
      # API expects string "null", not nil/JSON null
      variables["LISTEN_HTTPS_PORT"] = "null"
      variables["LISTEN_HTTP_PORT"] = (options[:http_port] || "80").to_s
    end

    # Reverse proxy configuration if specified
    if options[:reverse_proxy_host]
      variables["REVERSE_PROXY_HOST"] = options[:reverse_proxy_host]
      variables["REVERSE_PROXY_URL"] = options[:reverse_proxy_url] || "~ ^/(.*)$"

      # Real IP settings for reverse proxy
      unless options[:real_ip_from].nil?
        variables["USE_REAL_IP"] = "yes"
        variables["REAL_IP_FROM"] = options[:real_ip_from]
      end

      # Additional security settings from High template
      variables["USE_MODSECURITY"] = options[:use_modsecurity] || "yes"
      variables["USE_ANTIBOT"] = options[:anti_bot] || "captcha"
    end

    service_body = {
      server_name: service_name,
      is_draft: false,
      variables: variables
    }

    response = api_call("POST", "/services", {}, JSON.generate(service_body))

    # Accept both 201 (Created) and 200 (OK) for successful creation
    if [201, 200].include?(response[:status])
      puts "[INFO] Service '#{service_name}' created successfully"
    elsif response[:status] == 409
      raise BunkerWebApiError.new("Service '#{service_name}' already exists")
    else
      raise BunkerWebApiError.new("Failed to create service: status=#{response[:status]}, body=#{response[:raw_body]}")
    end

    return response[:body] || {}
  end

  def update_service_ssl(service_name, certificate_path, key_path, https_port = nil)
    # Update or change the SSL certificate path for an existing service.
    #
    # Args:
    #   service_name (String): Name of the service to update
    #   certificate_path (String): Path to the SSL certificate file
    #   key_path (String): Path to the SSL private key file
    #   https_port (Integer, optional): HTTPS port (default 443)
    # Returns: Hash with update response

    @extra_info = ""

    # First get current service configuration to preserve existing settings
    get_service_response = api_call("GET", "/services/#{service_name}", {})

    if get_service_response[:status] != 200
      raise BunkerWebApiError.new("Service '#{service_name}' not found")
    end

    # Extract current variables from the service configuration
    current_vars = get_service_response[:body]["variables"] || {}

    # Update SSL settings
    updated_vars = {
      "USE_SSL" => "yes",
      "SSL_CERTIFICATE_FILE_PATH" => certificate_path,
      "SSL_KEY_FILE_PATH" => key_path,
      "LISTEN_HTTPS_PORT" => (https_port || "443").to_s,
      "USE_REVERSE_PROXY_SSL" => "yes"
    }

    # Merge with existing variables (keep non-SSL settings)
    final_vars = current_vars.merge(updated_vars)

    service_body = {
      server_name: nil,  # Not changing name
      is_draft: false,   # Keep as online
      variables: final_vars
    }

    response = api_call("PATCH", "/services/#{service_name}", {}, JSON.generate(service_body))

    if response[:status] == 200
      puts "[INFO] SSL configuration updated for service '#{service_name}'"
    else
      raise BunkerWebApiError.new("Failed to update SSL configuration: status=#{response[:status]}, body=#{response[:raw_body]}")
    end

    return response[:body] || {}
  end

  def delete_service(service_name)
    # Delete a service by its name.
    #
    # Args:
    #   service_name (String): Name of the service to delete
    # Returns: Hash with deletion response

    @extra_info = ""
    # Verify service exists first
    get_response = api_call("GET", "/services/#{service_name}", {})

    if get_response[:status] != 200
      raise BunkerWebApiError.new("Service '#{service_name}' not found")
    end

    response = api_call("DELETE", "/services/#{service_name}")

    if response[:status] == 200 || response[:status] == 204
      puts "[INFO] Service '#{service_name}' deleted successfully"
      return response[:body] || {}
    else
      raise BunkerWebApiError.new("Failed to delete service: status=#{response[:status]}, body=#{response[:raw_body]}")
    end
  end

  # === Additional Utility Methods ===

  def list_services(drafts = false)
    # List all services.
    #
    # Args:
    #   drafts (Boolean): Include draft services (default: false, set to true to include drafts)
    # Returns: Array of service objects

    @extra_info = ""
    response = api_call("GET", "/services")

    if response[:status] != 200
      raise BunkerWebApiError.new("Failed to list services: status=#{response[:status]}")
    end

    return response[:body] || []
  end

  def get_service(service_name)
    # Get details of a specific service.
    #
    # Args:
    #   service_name (String): Name of the service to retrieve
    # Returns: Hash with service configuration

    @extra_info = ""
    response = api_call("GET", "/services/#{service_name}")

    if response[:status] != 200
      raise BunkerWebApiError.new("Service '#{service_name}' not found")
    end

    return response[:body] || {}
  end

  def reload_instance(instance_hostname = nil)
    # Reload configuration on an instance.
    #
    # Args:
    #   instance_hostname (String, optional): Instance hostname to reload (if nil, reloads all instances)
    # Returns: Hash with reload response

    @extra_info = ""
    path = if instance_hostname.nil?
            "/instances/reload"
          else
            "/instances/#{instance_hostname}/reload"
          end

    response = api_call("POST", "#{path}?test=no")

    if response[:status] == 200 || response[:status] == 201
      puts "[INFO] Configuration reloaded successfully"
      return response[:body] || {}
    else
      raise BunkerWebApiError.new("Failed to reload configuration: status=#{response[:status]}")
    end
  end

  def list_instances()
    # List all registered instances.
    # Returns: Array of instance objects

    @extra_info = ""
    response = api_call("GET", "/instances")

    if response[:status] != 200
      raise BunkerWebApiError.new("Failed to list instances: status=#{response[:status]}")
    end

    return response[:body] || []
  end

  def create_instance(hostname, name = nil, port = 8888, https_port = nil)
    # Create/register a new BunkerWeb instance (worker node).
    #
    # Args:
    #   hostname (String): IP address or hostname of the worker node
    #   name (String, optional): Human-readable name for the instance
    #   port (Integer): API port on the worker node (default 8888)
    #   https_port (Integer, optional): HTTPS port
    # Returns: Hash with creation response

    @extra_info = ""
    instance_body = {
      hostname: hostname,
      name: name || "BunkerWeb Instance",
      port: port,
      listen_https: !https_port.nil?,
      https_port: https_port,
      server_name: hostname,
      method: "api"  # Using API deployment method
    }

    response = api_call("POST", "/instances", {}, JSON.generate(instance_body))

    if response[:status] == 201
      puts "[INFO] Instance '#{hostname}' registered successfully"
    elsif response[:status] == 409
      # Instance already exists - that's OK, we just want to use it
      puts "[INFO] Instance '#{hostname}' already exists, will be used for this service"
    else
      raise BunkerWebApiError.new("Failed to create instance: status=#{response[:status]}, body=#{response[:raw_body]}")
    end

    return response[:body] || {}
  end

  def delete_instance(hostname)
    """
    Delete a registered instance.

    Args:
        hostname (String): Hostname of the instance to delete
    """

    @extra_info = ""
    response = api_call("DELETE", "/instances/#{hostname}")

    if response[:status] == 200 || response[:status] == 204
      puts "[INFO] Instance '#{hostname}' deleted successfully"
      return true
    else
      raise BunkerWebApiError.new("Failed to delete instance: status=#{response[:status]}")
    end
  end
end

# === Example Usage (can be run as script) ===

if __FILE__ == $0
  # Example usage demonstration
  begin
    api = HestiaBunkerWebApi.new(
      "http://127.0.0.1:8888",
      "admin",
      "your_password"
    )

    puts ""
    puts "[INFO] Creating service 'example.my.domain'"
    result = api.create_service("example.my.domain", {
      reverse_proxy_host: "http://192.168.3.51:8078",
      ssl: "no",
      use_template: "high"
    })

    puts ""
    puts "[INFO] Listing services:"
    services = api.list_services()
    services.each { |s| puts "- #{s['server_name']}" }

  rescue BunkerWebApiError => e
    puts "[ERROR] #{e.message}"
    exit 1
  end
end
