require "http/client"
require "json"
require "uri"
require "openssl"
require "random/secure"
require "base64"
require "josie-health-utils"

class AuthHandler
  Log = JosieHealth::Utils::Log
  @redis_client : RedisClient
  @discord_client_id : String
  @discord_client_secret : String
  @redirect_uri : String

  MAX_KEYS_PER_USER = 5

  def initialize(redis_client : RedisClient)
    @redis_client = redis_client
    @discord_client_id = ENV["DISCORD_CLIENT_ID"]? || ""
    @discord_client_secret = ENV["DISCORD_CLIENT_SECRET"]? || ""
    @redirect_uri = ENV["DISCORD_REDIRECT_URI"]? || "https://api.josie.health/oauth/callback"

    if @discord_client_id.empty?
      Log.warn("auth", "DISCORD_CLIENT_ID not set - OAuth2 disabled")
    end
  end

  def handle(request : HTTP::Request, response : HTTP::Server::Response, authenticated_user : String? = nil)
    case request.path
    when "/oauth/start"
      handle_oauth_start(request, response)
    when "/oauth/callback"
      handle_oauth_callback(request, response)
    when "/v1/auth/keys"
      case request.method
      when "GET"
        handle_list_keys(request, response, authenticated_user)
      when "POST"
        handle_create_key(request, response, authenticated_user)
      else
        method_not_allowed(response)
      end
    when .starts_with?("/v1/auth/keys/")
      if request.method == "DELETE"
        handle_revoke_key(request, response, authenticated_user)
      else
        method_not_allowed(response)
      end
    when "/v1/auth/irc/request"
      handle_irc_request(request, response) if request.method == "POST"
    when "/v1/auth/irc/verify"
      handle_irc_verify(request, response) if request.method == "POST"
    when "/v1/auth/irc/complete"
      handle_irc_complete(request, response) if request.method == "POST"
    when "/v1/auth/irc/status"
      handle_irc_status(request, response) if request.method == "GET"
    when "/v1/accounts/link"
      handle_link_accounts(request, response, authenticated_user) if request.method == "POST"
    when "/v1/accounts/linked"
      handle_get_linked(request, response, authenticated_user) if request.method == "GET"
    else
      response.status_code = 404
      response.print({"error" => "Not found"}.to_json)
    end
  end

  private def handle_oauth_start(request : HTTP::Request, response : HTTP::Server::Response)
    if @discord_client_id.empty?
      response.status_code = 503
      response.print({"error" => "OAuth2 not configured"}.to_json)
      return
    end

    params = request.query_params
    from = params["from"]? || "swagger"
    discord_id = params["discord_id"]?

    # Generate state token for CSRF protection
    state = Random::Secure.hex(16)

    # Store state with metadata (10 minute TTL)
    state_data = {"from" => from}
    state_data["discord_id"] = discord_id if discord_id
    @redis_client.store_oauth_state(state, state_data)

    # Build Discord OAuth2 URL
    discord_auth_url = String.build do |str|
      str << "https://discord.com/api/oauth2/authorize?"
      str << "client_id=#{URI.encode_www_form(@discord_client_id)}"
      str << "&redirect_uri=#{URI.encode_www_form(@redirect_uri)}"
      str << "&response_type=code"
      str << "&scope=identify"
      str << "&state=#{state}"
    end

    # Redirect to Discord
    response.status_code = 302
    response.headers["Location"] = discord_auth_url
  end

  private def handle_oauth_callback(request : HTTP::Request, response : HTTP::Server::Response)
    params = request.query_params
    code = params["code"]?
    state = params["state"]?
    error = params["error"]?

    if error
      show_error_page(response, "Discord authorization was denied: #{error}")
      return
    end

    unless code && state
      show_error_page(response, "Missing code or state parameter")
      return
    end

    # Verify and consume state token
    state_data = @redis_client.consume_oauth_state(state)
    unless state_data
      show_error_page(response, "Invalid or expired state token")
      return
    end

    from = state_data["from"]? || "swagger"

    # Exchange code for access token
    token_response = exchange_code_for_token(code)
    unless token_response
      show_error_page(response, "Failed to exchange code for token")
      return
    end

    access_token = token_response["access_token"]?.try(&.as_s)
    unless access_token
      show_error_page(response, "No access token in response")
      return
    end

    # Get Discord user info
    discord_user = get_discord_user(access_token)
    unless discord_user
      show_error_page(response, "Failed to get Discord user info")
      return
    end

    discord_id = discord_user["id"]?.try(&.as_s) || ""
    discord_username = discord_user["username"]?.try(&.as_s) || ""

    if discord_id.empty? || discord_username.empty?
      show_error_page(response, "Invalid Discord user data")
      return
    end

    # Check if user already has a mapping, otherwise use Discord username
    user_id = @redis_client.get_user_by_discord_id(discord_id) || discord_username.downcase

    # Store/update Discord mapping
    @redis_client.store_discord_mapping(discord_id, user_id)

    # Check key limit
    if @redis_client.count_user_keys(user_id) >= MAX_KEYS_PER_USER
      show_error_page(response, "Maximum API keys (#{MAX_KEYS_PER_USER}) reached. Revoke an existing key first.")
      return
    end

    # Generate new API key
    api_key = generate_api_key
    key_hash = hash_api_key(api_key)

    # Store the key
    @redis_client.store_api_key(key_hash, user_id, discord_id, "oauth-generated")

    Log.info("auth", "Generated API key for user via OAuth")

    # Show success page with the key
    show_success_page(response, api_key, user_id, from)
  end

  private def handle_list_keys(request : HTTP::Request, response : HTTP::Server::Response, authenticated_user : String?)
    # For API key auth, use the authenticated user
    # For Basic auth, require user_id query param
    user_id = authenticated_user || request.query_params["user_id"]?

    unless user_id
      response.status_code = 400
      response.print({"error" => "Missing user_id (use API key auth or provide user_id param)"}.to_json)
      return
    end

    keys = @redis_client.list_user_keys(user_id)

    response.print({
      "data" => keys,
      "meta" => {
        "user_id" => user_id,
        "count"   => keys.size,
        "limit"   => MAX_KEYS_PER_USER,
      },
    }.to_json)
  end

  private def handle_create_key(request : HTTP::Request, response : HTTP::Server::Response, authenticated_user : String?)
    user_id = authenticated_user || begin
      body = request.body.try(&.gets_to_end) || ""
      data = JSON.parse(body) rescue nil
      data.try(&.["user_id"]?).try(&.as_s)
    end

    unless user_id
      response.status_code = 400
      response.print({"error" => "Missing user_id"}.to_json)
      return
    end

    # Check key limit
    if @redis_client.count_user_keys(user_id) >= MAX_KEYS_PER_USER
      response.status_code = 429
      response.print({"error" => "Maximum API keys (#{MAX_KEYS_PER_USER}) reached"}.to_json)
      return
    end

    # Parse optional name from body
    body = request.body.try(&.gets_to_end) || ""
    data = JSON.parse(body) rescue nil
    name = data.try(&.["name"]?).try(&.as_s) || "api-generated"
    discord_id = data.try(&.["discord_id"]?).try(&.as_s) || ""

    # Generate new API key
    api_key = generate_api_key
    key_hash = hash_api_key(api_key)

    @redis_client.store_api_key(key_hash, user_id, discord_id, name)

    response.status_code = 201
    response.print({
      "data" => {
        "api_key" => api_key,
        "prefix"  => key_hash[0, 8],
        "user_id" => user_id,
        "name"    => name,
      },
      "meta" => {
        "warning" => "Store this key securely - it cannot be retrieved again",
      },
    }.to_json)
  end

  private def handle_revoke_key(request : HTTP::Request, response : HTTP::Server::Response, authenticated_user : String?)
    # Extract prefix from path: /v1/auth/keys/{prefix}
    prefix = request.path.sub("/v1/auth/keys/", "")

    user_id = authenticated_user || request.query_params["user_id"]?

    unless user_id
      response.status_code = 400
      response.print({"error" => "Missing user_id"}.to_json)
      return
    end

    if @redis_client.revoke_api_key_by_prefix(user_id, prefix)
      response.print({"data" => {"revoked" => true, "prefix" => prefix}}.to_json)
    else
      response.status_code = 404
      response.print({"error" => "Key not found or already revoked"}.to_json)
    end
  end

  private def exchange_code_for_token(code : String) : JSON::Any?
    client = HTTP::Client.new("discord.com", tls: true)

    body = HTTP::Params.build do |form|
      form.add "client_id", @discord_client_id
      form.add "client_secret", @discord_client_secret
      form.add "grant_type", "authorization_code"
      form.add "code", code
      form.add "redirect_uri", @redirect_uri
    end

    headers = HTTP::Headers{
      "Content-Type" => "application/x-www-form-urlencoded",
    }

    response = client.post("/api/oauth2/token", headers: headers, body: body)

    if response.success?
      JSON.parse(response.body)
    else
      Log.warn("auth", "Discord token exchange failed: #{response.status_code}")
      nil
    end
  rescue ex
    Log.error("auth", "Error exchanging Discord code: #{ex.message}")
    nil
  end

  private def get_discord_user(access_token : String) : JSON::Any?
    client = HTTP::Client.new("discord.com", tls: true)

    headers = HTTP::Headers{
      "Authorization" => "Bearer #{access_token}",
    }

    response = client.get("/api/users/@me", headers: headers)

    if response.success?
      JSON.parse(response.body)
    else
      Log.warn("auth", "Discord user fetch failed: #{response.status_code}")
      nil
    end
  rescue ex
    Log.error("auth", "Error fetching Discord user: #{ex.message}")
    nil
  end

  private def generate_api_key : String
    # 32 bytes = 256 bits of entropy
    random_bytes = Random::Secure.random_bytes(32)
    "jhapi_#{Base64.urlsafe_encode(random_bytes, padding: false)}"
  end

  private def hash_api_key(api_key : String) : String
    OpenSSL::Digest.new("SHA256").update(api_key).final.hexstring
  end

  private def method_not_allowed(response : HTTP::Server::Response)
    response.status_code = 405
    response.print({"error" => "Method not allowed"}.to_json)
  end

  private def show_error_page(response : HTTP::Server::Response, message : String)
    response.content_type = "text/html"
    response.status_code = 400
    response.print(<<-HTML)
    <!DOCTYPE html>
    <html>
    <head>
      <title>Error - Josie Health API</title>
      <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
        .error { background: #fee; border: 1px solid #c00; padding: 20px; border-radius: 8px; }
        h1 { color: #c00; }
      </style>
    </head>
    <body>
      <div class="error">
        <h1>Authentication Error</h1>
        <p>#{message}</p>
        <p><a href="/">Return to API documentation</a></p>
      </div>
    </body>
    </html>
    HTML
  end

  private def show_success_page(response : HTTP::Server::Response, api_key : String, user_id : String, from : String)
    response.content_type = "text/html"
    response.print(<<-HTML)
    <!DOCTYPE html>
    <html>
    <head>
      <title>API Key Generated - Josie Health API</title>
      <style>
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 700px; margin: 50px auto; padding: 20px; background: #f5f5f5; }
        .card { background: white; border: 1px solid #000; padding: 30px; border-radius: 0; }
        h1 { color: #000; margin-top: 0; }
        .key-box { background: #f8f8f8; border: 1px solid #000; padding: 15px; font-family: monospace; font-size: 14px; word-break: break-all; margin: 20px 0; }
        .warning { background: #fff3cd; border: 1px solid #000; padding: 15px; margin: 20px 0; }
        .info { background: #e7f3ff; border: 1px solid #000; padding: 15px; margin: 20px 0; }
        button { background: #000; color: white; border: none; padding: 10px 20px; cursor: pointer; font-size: 14px; }
        button:hover { background: #333; }
        code { background: #f0f0f0; padding: 2px 6px; }
      </style>
    </head>
    <body>
      <div class="card">
        <h1>API Key Generated</h1>
        <p>Welcome, <strong>#{user_id}</strong>!</p>

        <div class="warning">
          <strong>Important:</strong> Copy this key now. It cannot be retrieved again.
        </div>

        <div class="key-box" id="api-key">#{api_key}</div>

        <button onclick="copyKey()">Copy to Clipboard</button>

        <div class="info">
          <strong>Usage:</strong><br>
          Add this header to your API requests:<br>
          <code>Authorization: Bearer #{api_key}</code>
        </div>

        <p>
          <a href="/">Use in Swagger UI</a> |
          <a href="/v1/auth/keys?user_id=#{user_id}">View all keys</a>
        </p>
      </div>

      <script>
        function copyKey() {
          const key = document.getElementById('api-key').textContent;
          navigator.clipboard.writeText(key).then(() => {
            alert('API key copied to clipboard!');
          });
        }
      </script>
    </body>
    </html>
    HTML
  end

  private def handle_irc_request(request : HTTP::Request, response : HTTP::Server::Response)
    body = request.body.try(&.gets_to_end) || ""
    data = JSON.parse(body) rescue nil

    network = data.try(&.["network"]?).try(&.as_s) || ""

    if network.empty?
      response.status_code = 400
      response.print({"error" => "Missing network parameter"}.to_json)
      return
    end

    token = Random::Secure.hex(16)

    @redis_client.store_irc_auth_request(token, {
      "network"    => network,
      "created_at" => Time.utc.to_rfc3339,
    })

    Log.info("auth", "IRC auth request created for network #{network}")

    response.status_code = 201
    response.print({
      "data" => {
        "token"        => token,
        "network"      => network,
        "bot_command"  => "VERIFY #{token}",
        "expires_in"   => 600,
        "instructions" => "Send this command to the bot: /msg annotate VERIFY #{token}",
      },
    }.to_json)
  end

  private def handle_irc_verify(request : HTTP::Request, response : HTTP::Server::Response)
    body = request.body.try(&.gets_to_end) || ""
    data = JSON.parse(body) rescue nil

    token = data.try(&.["token"]?).try(&.as_s) || ""
    nick = data.try(&.["nick"]?).try(&.as_s) || ""
    account_name = data.try(&.["account_name"]?).try(&.as_s) || ""

    if token.empty? || nick.empty?
      response.status_code = 400
      response.print({"error" => "Missing token or nick"}.to_json)
      return
    end

    auth_request = @redis_client.get_irc_auth_request(token)
    unless auth_request
      response.status_code = 404
      response.print({"error" => "Invalid or expired token"}.to_json)
      return
    end

    @redis_client.mark_irc_auth_verified(token, nick, account_name)

    Log.info("auth", "IRC auth verified for #{nick} on #{auth_request["network"]?}")

    response.print({
      "data" => {
        "verified" => true,
        "nick"     => nick,
        "network"  => auth_request["network"]?,
      },
    }.to_json)
  end

  private def handle_irc_status(request : HTTP::Request, response : HTTP::Server::Response)
    token = request.query_params["token"]? || ""

    if token.empty?
      response.status_code = 400
      response.print({"error" => "Missing token parameter"}.to_json)
      return
    end

    auth_request = @redis_client.get_irc_auth_request(token)
    unless auth_request
      response.status_code = 404
      response.print({"error" => "Invalid or expired token"}.to_json)
      return
    end

    verified = auth_request["verified"]? == "true"

    response.print({
      "data" => {
        "token"    => token,
        "verified" => verified,
        "nick"     => auth_request["nick"]?,
        "network"  => auth_request["network"]?,
      },
    }.to_json)
  end

  private def handle_irc_complete(request : HTTP::Request, response : HTTP::Server::Response)
    body = request.body.try(&.gets_to_end) || ""
    data = JSON.parse(body) rescue nil

    token = data.try(&.["token"]?).try(&.as_s) || ""

    if token.empty?
      response.status_code = 400
      response.print({"error" => "Missing token"}.to_json)
      return
    end

    auth_data = @redis_client.consume_irc_auth(token)
    unless auth_data
      response.status_code = 400
      response.print({"error" => "Token not verified or expired"}.to_json)
      return
    end

    nick = auth_data["nick"]? || ""
    network = auth_data["network"]? || ""
    account_name = auth_data["account_name"]? || nick

    user_id = @redis_client.get_user_by_irc(network, nick) || account_name.downcase
    @redis_client.store_irc_user_mapping(network, nick, user_id)

    Log.info("auth", "IRC auth completed for #{nick}@#{network} -> #{user_id}")

    response.print({
      "data" => {
        "user_id"      => user_id,
        "nick"         => nick,
        "network"      => network,
        "account_name" => account_name,
      },
    }.to_json)
  end

  private def handle_link_accounts(request : HTTP::Request, response : HTTP::Server::Response, authenticated_user : String?)
    body = request.body.try(&.gets_to_end) || ""
    data = JSON.parse(body) rescue nil

    primary_user = authenticated_user || data.try(&.["primary_user"]?).try(&.as_s)
    secondary_user = data.try(&.["secondary_user"]?).try(&.as_s)
    method = data.try(&.["method"]?).try(&.as_s) || "manual"

    unless primary_user && secondary_user
      response.status_code = 400
      response.print({"error" => "Missing primary_user or secondary_user"}.to_json)
      return
    end

    if authenticated_user && authenticated_user != primary_user
      response.status_code = 403
      response.print({"error" => "Forbidden: you can only link accounts to your own"}.to_json)
      return
    end

    if primary_user == secondary_user
      response.status_code = 400
      response.print({"error" => "Cannot link account to itself"}.to_json)
      return
    end

    existing = @redis_client.get_primary_account(secondary_user)
    if existing
      response.status_code = 400
      response.print({"error" => "Secondary account already linked to #{existing}"}.to_json)
      return
    end

    migrated = @redis_client.migrate_doses(secondary_user, primary_user)
    @redis_client.link_accounts(primary_user, secondary_user, method)

    Log.info("auth", "Linked accounts: #{secondary_user} -> #{primary_user}, migrated #{migrated} doses")

    response.print({
      "data" => {
        "primary_user"   => primary_user,
        "secondary_user" => secondary_user,
        "doses_migrated" => migrated,
      },
    }.to_json)
  end

  private def handle_get_linked(request : HTTP::Request, response : HTTP::Server::Response, authenticated_user : String?)
    user_id = authenticated_user || request.query_params["user_id"]?

    unless user_id
      response.status_code = 400
      response.print({"error" => "Missing user_id"}.to_json)
      return
    end

    linked = @redis_client.get_linked_accounts(user_id)
    primary = @redis_client.get_primary_account(user_id)

    response.print({
      "data" => {
        "user_id"         => user_id,
        "linked_accounts" => linked,
        "primary_account" => primary,
      },
    }.to_json)
  end
end
