// Server Action Client
//
// Client-side utilities for invoking server actions.
// This module provides type-safe action invocation from the browser.

// ============================================================================
// Action Client Types
// ============================================================================

///|
/// Action invocation options
pub struct ActionOptions {
  /// Custom headers to send with the request
  headers : Array[(String, String)]
  /// Request timeout in milliseconds
  timeout : Int?
  /// Abort signal (for cancellation)
  signal : @js_async.AbortSignal?
}

///|
/// Default action options
pub fn ActionOptions::default() -> ActionOptions {
  { headers: [], timeout: None, signal: None }
}

///|
/// Action client response
priv enum ActionResponse {
  /// Successful response with data
  Success(@js.Any)
  /// Server requested a redirect
  Redirect(String)
  /// Error response
  Error(Int, String)
  /// Network error
  NetworkError(String)
}

///|
/// Typed action client response.
pub enum TypedActionResponse[Res] {
  /// Successful response decoded into the generated response payload type
  Success(Res)
  /// Server requested a redirect
  Redirect(String)
  /// Error response
  Error(Int, String)
  /// Network error
  NetworkError(String)
  /// Successful HTTP response whose JSON shape did not match `Res`
  DecodeError(String)
}

///|
/// Decode an untyped action response into a typed action response.
fn[Res : FromJson] TypedActionResponse::from_action_response(
  response : ActionResponse,
) -> TypedActionResponse[Res] {
  match response {
    ActionResponse::Success(data) => {
      guard @js_any.any_to_json(data) is Some(json) else {
        return TypedActionResponse::DecodeError("Action response is not JSON")
      }
      let decoded : Res = @json.from_json(json) catch {
        _ =>
          return TypedActionResponse::DecodeError(
            "Action response does not match expected type",
          )
      }
      TypedActionResponse::Success(decoded)
    }
    ActionResponse::Redirect(url) => TypedActionResponse::Redirect(url)
    ActionResponse::Error(status, message) =>
      TypedActionResponse::Error(status, message)
    ActionResponse::NetworkError(message) =>
      TypedActionResponse::NetworkError(message)
  }
}

///|
/// UI-facing action phase for common form/action states
pub enum ActionPhase {
  Idle
  Pending
  Succeeded
  Failed
  Redirecting
} derive(Eq, Debug)

///|
/// UI-facing action state for pending/error/success controls
pub struct ActionState {
  phase : ActionPhase
  status : Int?
  message : String?
  redirect : String?
}

///|
/// Initial idle action state
pub fn ActionState::idle() -> ActionState {
  { phase: Idle, status: None, message: None, redirect: None }
}

///|
/// Pending action state
pub fn ActionState::pending(message? : String = "Pending") -> ActionState {
  { phase: Pending, status: None, message: Some(message), redirect: None }
}

///|
/// Successful action state
pub fn ActionState::success(message? : String = "Success") -> ActionState {
  { phase: Succeeded, status: None, message: Some(message), redirect: None }
}

///|
/// Failed action state
pub fn ActionState::error(status~ : Int, message~ : String) -> ActionState {
  {
    phase: Failed,
    status: Some(status),
    message: Some(message),
    redirect: None,
  }
}

///|
/// Redirecting action state
pub fn ActionState::redirect(url : String) -> ActionState {
  {
    phase: Redirecting,
    status: None,
    message: Some("Redirecting"),
    redirect: Some(url),
  }
}

// ============================================================================
// Action Invocation
// ============================================================================

///|
/// Invoke a server action with callback
/// - `url`: The action endpoint URL (e.g., "/_action/create-user")
/// - `payload`: The JSON payload to send
/// - `callback`: Function to call with the response
/// - `options`: Optional configuration
fn invoke_action(
  url : String,
  payload : @js.Any,
  callback : (ActionResponse) -> Unit,
  options? : ActionOptions,
) -> Unit {
  let opts = options.unwrap_or(ActionOptions::default())
  ffi_invoke_action_cb(url, payload, callback, opts)
}

///|
/// Invoke a typed server action using its generated request payload type.
pub fn[Req : ToJson, Res : FromJson] invoke_typed_action_key(
  key : TypedActionKey[Req, Res],
  payload : Req,
  callback : (TypedActionResponse[Res]) -> Unit,
  options? : ActionOptions,
) -> Unit {
  invoke_action(
    typed_action_url(key),
    @js_any.json_to_any(payload.to_json()),
    fn(response) {
      callback(TypedActionResponse::from_action_response(response))
    },
    options?,
  )
}

///|
/// Create an action invoker whose payload parameter is tied to a typed key.
pub fn[Req : ToJson, Res : FromJson] create_typed_action_invoker_key(
  key : TypedActionKey[Req, Res],
  options? : ActionOptions,
) -> (Req, (TypedActionResponse[Res]) -> Unit) -> Unit {
  let url = typed_action_url(key)
  let opts = options.unwrap_or(ActionOptions::default())
  fn(payload, callback) {
    ffi_invoke_action_cb(
      url,
      @js_any.json_to_any(payload.to_json()),
      fn(response) {
        callback(TypedActionResponse::from_action_response(response))
      },
      opts,
    )
  }
}

// ============================================================================
// Form Integration
// ============================================================================

///|
/// Action form configuration
pub struct ActionFormConfig[Res] {
  /// Action URL
  priv action : String
  /// Method (default: POST)
  priv form_method : String
  /// On success callback
  priv on_success : ((Res) -> Unit)?
  /// On error callback
  priv on_error : ((Int, String) -> Unit)?
  /// On redirect callback (return false to prevent redirect)
  priv on_redirect : ((String) -> Bool)?
}

///|
/// Raw JS-facing form configuration.
priv struct RawActionFormConfig {
  action : String
  form_method : String
  on_success : ((@js.Any) -> Unit)?
  on_error : ((Int, String) -> Unit)?
  on_redirect : ((String) -> Bool)?
}

///|
/// Default form config
fn[Res] ActionFormConfig::new(action : String) -> ActionFormConfig[Res] {
  {
    action,
    form_method: "POST",
    on_success: None,
    on_error: None,
    on_redirect: None,
  }
}

///|
/// Create form config from a generated action key
pub fn[Req, Res] ActionFormConfig::from_key(
  key : TypedActionKey[Req, Res],
) -> ActionFormConfig[Res] {
  ActionFormConfig::new(typed_action_url(key))
}

///|
/// Set form method for action form submission.
pub fn[Res] ActionFormConfig::with_method(
  self : ActionFormConfig[Res],
  form_method : String,
) -> ActionFormConfig[Res] {
  { ..self, form_method, }
}

///|
/// Set success callback for action form submission.
pub fn[Res] ActionFormConfig::with_success(
  self : ActionFormConfig[Res],
  on_success : (Res) -> Unit,
) -> ActionFormConfig[Res] {
  { ..self, on_success: Some(on_success) }
}

///|
/// Set error callback for action form submission.
pub fn[Res] ActionFormConfig::with_error(
  self : ActionFormConfig[Res],
  on_error : (Int, String) -> Unit,
) -> ActionFormConfig[Res] {
  { ..self, on_error: Some(on_error) }
}

///|
/// Set redirect callback for action form submission.
pub fn[Res] ActionFormConfig::with_redirect(
  self : ActionFormConfig[Res],
  on_redirect : (String) -> Bool,
) -> ActionFormConfig[Res] {
  { ..self, on_redirect: Some(on_redirect) }
}

///|
fn[Res : FromJson] ActionFormConfig::to_raw(
  self : ActionFormConfig[Res],
) -> RawActionFormConfig {
  let on_success : ((@js.Any) -> Unit)? = match self.on_success {
    Some(callback) =>
      Some(fn(data) {
        guard @js_any.any_to_json(data) is Some(json) else {
          match self.on_error {
            Some(on_error) => on_error(0, "Action form response is not JSON")
            None => ()
          }
          return
        }
        let decoded : Res = @json.from_json(json) catch {
          _ => {
            match self.on_error {
              Some(on_error) =>
                on_error(0, "Action form response does not match expected type")
              None => ()
            }
            return
          }
        }
        callback(decoded)
      })
    None => None
  }
  {
    action: self.action,
    form_method: self.form_method,
    on_success,
    on_error: self.on_error,
    on_redirect: self.on_redirect,
  }
}

///|
/// Handle form submission as server action
pub fn[Res : FromJson] submit_form_as_action(
  form : @js_dom.HTMLFormElement,
  config : ActionFormConfig[Res],
) -> Unit {
  ffi_submit_form_action(form, config.to_raw())
}

// ============================================================================
// FFI Implementation
// ============================================================================

// TODO: MoonBit enum serialization format
// Currently using $tag (numeric index) format for enum variants:
//   { $tag: 0, _0: data }     // Success
//   { $tag: 1, _0: url }      // Redirect
//   { $tag: 2, _0: status, _1: msg }  // Error
//   { $tag: 3, _0: msg }      // NetworkError
//
// Issues to investigate:
// 1. Is $tag the stable/official format? Or should we use $ with string names?
// 2. MoonBit's Option uses { $: "Some", _0: value } but enums use $tag
// 3. Consider creating a helper function or macro for enum construction
// 4. Document the serialization format in MoonBit FFI best practices
//
// See: docs/internal/server-action-implementation.md

///|
extern "js" fn ffi_invoke_action_cb(
  url : String,
  payload : @js.Any,
  callback : (ActionResponse) -> Unit,
  options : ActionOptions,
) -> Unit =
  #| (url, payload, callback, options) => {
  #|   (async () => {
  #|     try {
  #|       // Build headers - options.headers is MoonBit Array of tuples
  #|       const headers = { 'Content-Type': 'application/json' };
  #|       if (options.headers && options.headers.length > 0) {
  #|         for (const h of options.headers) {
  #|           // MoonBit tuple: h[0] or h._0 for first, h[1] or h._1 for second
  #|           const key = h[0] ?? h._0;
  #|           const val = h[1] ?? h._1;
  #|           if (key && val) headers[key] = val;
  #|         }
  #|       }
  #|
  #|       const fetchOptions = {
  #|         method: 'POST',
  #|         headers,
  #|         body: JSON.stringify(payload),
  #|         credentials: 'same-origin'
  #|       };
  #|
  #|       // Handle MoonBit Option type: { $: "Some", _0: value } or { $: "None" }
  #|       const signal = options.signal;
  #|       if (signal && signal.$ === 'Some' && signal._0 instanceof AbortSignal) {
  #|         fetchOptions.signal = signal._0;
  #|       }
  #|
  #|       const response = await fetch(url, fetchOptions);
  #|
  #|       if (!response.ok) {
  #|         const text = await response.text();
  #|         try {
  #|           const json = JSON.parse(text);
  #|           // Error = variant index 2
  #|           callback({ $tag: 2, _0: response.status, _1: json.error || text });
  #|         } catch {
  #|           callback({ $tag: 2, _0: response.status, _1: text });
  #|         }
  #|         return;
  #|       }
  #|
  #|       const data = await response.json();
  #|
  #|       // Check for redirect instruction
  #|       if (data.redirect) {
  #|         // Redirect = variant index 1
  #|         callback({ $tag: 1, _0: data.redirect });
  #|         return;
  #|       }
  #|
  #|       // Success = variant index 0
  #|       callback({ $tag: 0, _0: data });
  #|     } catch (error) {
  #|       // NetworkError = variant index 3
  #|       callback({ $tag: 3, _0: error.message || 'Network error' });
  #|     }
  #|   })();
  #| }

///|
extern "js" fn ffi_submit_form_action(
  form : @js_dom.HTMLFormElement,
  config : RawActionFormConfig,
) -> Unit =
  #| async (form, config) => {
  #|   try {
  #|     const formData = new FormData(form);
  #|     const payload = Object.fromEntries(formData.entries());
  #|
  #|     const response = await fetch(config.action, {
  #|       method: config.form_method || 'POST',
  #|       headers: { 'Content-Type': 'application/json' },
  #|       body: JSON.stringify(payload),
  #|       credentials: 'same-origin'
  #|     });
  #|
  #|     const data = await response.json();
  #|
  #|     if (!response.ok) {
  #|       if (config.on_error) {
  #|         config.on_error(response.status, data.error || 'Unknown error');
  #|       }
  #|       return;
  #|     }
  #|
  #|     if (data.redirect) {
  #|       const shouldRedirect = config.on_redirect ? config.on_redirect(data.redirect) : true;
  #|       if (shouldRedirect) {
  #|         window.location.href = data.redirect;
  #|       }
  #|       return;
  #|     }
  #|
  #|     if (config.on_success) {
  #|       config.on_success(data);
  #|     }
  #|   } catch (error) {
  #|     if (config.on_error) {
  #|       config.on_error(0, error.message || 'Network error');
  #|     }
  #|   }
  #| }