///|
/// Endpoint methods, ported from `src/api-endpoint-methods.ts`.
///
/// Each resource is exposed as a struct of bound functions (for example
/// `client.pages().create(args)`), so the shape matches the TypeScript client's
/// `notion.pages.create` while staying idiomatic MoonBit.

///|
/// Resource methods for `users`.
pub struct UsersEndpointMethods {
  me : (Json) -> Json raise
  retrieve : (Json) -> Json raise
  list : (Json) -> Json raise
}

///|
/// Resource methods for `pages`.
pub struct PagesEndpointMethods {
  create : (Json) -> Json raise
  retrieve : (Json) -> Json raise
  update : (Json) -> Json raise
  move_page : (Json) -> Json raise
  retrieve_markdown : (Json) -> Json raise
  update_markdown : (Json) -> Json raise
  properties_retrieve : (Json) -> Json raise
}

///|
/// Resource methods for `asyncTasks`.
pub struct AsyncTasksEndpointMethods {
  retrieve : (Json) -> Json raise
}

///|
/// Resource methods for `blocks`.
pub struct BlocksEndpointMethods {
  retrieve : (Json) -> Json raise
  update : (Json) -> Json raise
  delete : (Json) -> Json raise
  children_list : (Json) -> Json raise
  children_append : (Json) -> Json raise
  meeting_notes_create : (Json) -> Json raise
  meeting_notes_query : (Json) -> Json raise
}

///|
/// Resource methods for `dataSources`.
pub struct DataSourcesEndpointMethods {
  retrieve : (Json) -> Json raise
  update : (Json) -> Json raise
  query : (Json) -> Json raise
  create : (Json) -> Json raise
  list_templates : (Json) -> Json raise
}

///|
/// Resource methods for `databases`.
pub struct DatabasesEndpointMethods {
  retrieve : (Json) -> Json raise
  update : (Json) -> Json raise
  create : (Json) -> Json raise
}

///|
/// Resource methods for `comments`.
pub struct CommentsEndpointMethods {
  create : (Json) -> Json raise
  list : (Json) -> Json raise
  retrieve : (Json) -> Json raise
  update : (Json) -> Json raise
  delete : (Json) -> Json raise
}

///|
/// Resource methods for `fileUploads`.
pub struct FileUploadsEndpointMethods {
  create : (Json) -> Json raise
  retrieve : (Json) -> Json raise
  list : (Json) -> Json raise
  send : (Json) -> Json raise
  complete : (Json) -> Json raise
}

///|
/// Resource methods for `views`.
pub struct ViewsEndpointMethods {
  list : (Json) -> Json raise
  create : (Json) -> Json raise
  retrieve : (Json) -> Json raise
  update : (Json) -> Json raise
  delete : (Json) -> Json raise
  queries_create : (Json) -> Json raise
  queries_results : (Json) -> Json raise
  queries_delete : (Json) -> Json raise
}

///|
/// Resource methods for `agents`.
pub struct AgentsEndpointMethods {
  query : (Json) -> Json raise
  retrieve : (Json) -> Json raise
  delete : (Json) -> Json raise
  retrieve_insights : (Json) -> Json raise
  update_status : (Json) -> Json raise
  update_credit_limit : (Json) -> Json raise
  batch : (Json) -> Json raise
}

///|
/// Resource methods for `plugins`.
pub struct PluginsEndpointMethods {
  list : (Json) -> Json raise
  retrieve : (Json) -> Json raise
}

///|
/// Resource methods for `skills`.
pub struct SkillsEndpointMethods {
  retrieve : (Json) -> Json raise
}

///|
/// Resource methods for `sessions`.
pub struct SessionsEndpointMethods {
  retrieve : (Json) -> Json raise
  update : (Json) -> Json raise
  stream : (Json) -> Array[Json] raise
  cancel : (Json) -> Json raise
  query : (Json) -> Json raise
  query_events : (Json) -> Json raise
}

///|
/// Resource methods for `oauth`.
pub struct OauthEndpointMethods {
  token : (Json) -> Json raise
  introspect : (Json) -> Json raise
  revoke : (Json) -> Json raise
}

///|
/// Arguments to `search`, exposed as its own resource in the TypeScript client.
pub struct SearchEndpointMethods {
  search : (Json) -> Json raise
}

///|
/// Calls an endpoint, filtering arguments through its parameter lists and
/// warning about unrecognized names.
fn Client::call_endpoint(
  self : Client,
  definition : @endpoints.EndpointDefinition,
  args : Json,
) -> Json raise {
  self.warn_unknown_params(args, definition)
  self.request(
    endpoint_request(
      definition,
      args,
      definition.body_params,
      definition.query_params,
    ),
  )
}

///|
/// Calls an endpoint with explicit body/query key lists. Used where the
/// TypeScript client filters fields itself (`views.create` and friends).
fn Client::call_endpoint_keys(
  self : Client,
  definition : @endpoints.EndpointDefinition,
  args : Json,
  body_keys : Array[String],
  query_keys : Array[String],
) -> Json raise {
  self.request(endpoint_request(definition, args, body_keys, query_keys))
}

///|
/// Calls an endpoint with a caller-supplied auth (used by the OAuth endpoints).
fn Client::call_endpoint_auth(
  self : Client,
  definition : @endpoints.EndpointDefinition,
  args : Json,
  auth : RequestAuth?,
) -> Json raise {
  self.warn_unknown_params(args, definition)
  let request = endpoint_request(
    definition,
    args,
    definition.body_params,
    definition.query_params,
  )
  self.request({ ..request, auth, })
}

///|
/// Builds the `RequestParameters` for an endpoint definition.
fn endpoint_request(
  definition : @endpoints.EndpointDefinition,
  args : Json,
  body_keys : Array[String],
  query_keys : Array[String],
) -> RequestParameters raise {
  RequestParameters::new(
    path=(definition.path)(args),
    http_method=definition.http_method,
    query=Some(pick(args, query_keys)),
    body=Some(pick(args, body_keys)),
    form_data_params=if definition.form_data_params.is_empty() {
      None
    } else {
      Some(pick(args, definition.form_data_params))
    },
    headers=definition.headers,
    auth=request_auth_from_args(args),
  )
}

///|
/// Reads a per-request bearer token from the `auth` field of an arguments
/// object.
fn request_auth_from_args(args : Json) -> RequestAuth? {
  match args {
    Json::Object(fields) =>
      match fields.get("auth") {
        Some(Json::String(token)) => Some(Bearer(token))
        _ => None
      }
    _ => None
  }
}

///|
/// Reads OAuth client credentials from an arguments object.
fn oauth_auth_from_args(args : Json) -> RequestAuth? {
  match args {
    Json::Object(fields) =>
      match (fields.get("client_id"), fields.get("client_secret")) {
        (Some(Json::String(client_id)), Some(Json::String(client_secret))) =>
          Some(Basic(client_id~, client_secret~))
        _ => None
      }
    _ => None
  }
}

///|
/// Logs a warning when the caller passes parameters that are not recognized by
/// the endpoint definition.
fn Client::warn_unknown_params(
  self : Client,
  args : Json,
  definition : @endpoints.EndpointDefinition,
) -> Unit {
  let unknown = get_unknown_params(args, definition)
  if !unknown.is_empty() {
    let known : Array[String] = []
    known.append(definition.path_params)
    known.append(definition.query_params)
    known.append(definition.body_params)
    known.append(definition.form_data_params)
    self.log_warning(unknown, known)
  }
}

///|
/// Emits the "unknown parameters were ignored" warning.
fn Client::log_warning(
  self : Client,
  unknown : Array[String],
  known : Array[String],
) -> Unit {
  self.log(
    Warn,
    "unknown parameters were ignored",
    Json::object(
      Map([
        ("unknownParams", Json::array(unknown.map(x => Json::string(x)))),
        ("knownParams", Json::array(known.map(x => Json::string(x)))),
      ]),
    ),
  )
}

///|
/// The resource clients, each bound to this client.
pub fn Client::users(self : Client) -> UsersEndpointMethods {
  {
    me: args => self.call_endpoint(@endpoints.getSelf, args),
    retrieve: args => self.call_endpoint(@endpoints.getUser, args),
    list: args => self.call_endpoint(@endpoints.listUsers, args),
  }
}

///|
pub fn Client::pages(self : Client) -> PagesEndpointMethods {
  {
    create: args => self.call_endpoint(@endpoints.createPage, args),
    retrieve: args => self.call_endpoint(@endpoints.getPage, args),
    update: args => self.call_endpoint(@endpoints.updatePage, args),
    move_page: args => self.call_endpoint(@endpoints.movePage, args),
    retrieve_markdown: args => {
      self.call_endpoint(@endpoints.getPageMarkdown, args)
    },
    update_markdown: args => {
      self.call_endpoint(@endpoints.updatePageMarkdown, args)
    },
    properties_retrieve: args => {
      self.call_endpoint(@endpoints.getPageProperty, args)
    },
  }
}

///|
pub fn Client::async_tasks(self : Client) -> AsyncTasksEndpointMethods {
  { retrieve: args => self.call_endpoint(@endpoints.getAsyncTask, args), }
}

///|
pub fn Client::blocks(self : Client) -> BlocksEndpointMethods {
  {
    retrieve: args => self.call_endpoint(@endpoints.getBlock, args),
    update: args => self.call_endpoint(@endpoints.updateBlock, args),
    delete: args => self.call_endpoint(@endpoints.deleteBlock, args),
    children_list: args => {
      self.call_endpoint(@endpoints.listBlockChildren, args)
    },
    children_append: args => {
      self.call_endpoint(@endpoints.appendBlockChildren, args)
    },
    meeting_notes_create: args => {
      self.call_endpoint(@endpoints.createMeetingNote, args)
    },
    meeting_notes_query: args => {
      self.call_endpoint(@endpoints.queryMeetingNotes, args)
    },
  }
}

///|
pub fn Client::data_sources(self : Client) -> DataSourcesEndpointMethods {
  {
    retrieve: args => self.call_endpoint(@endpoints.getDataSource, args),
    update: args => self.call_endpoint(@endpoints.updateDataSource, args),
    query: args => self.call_endpoint(@endpoints.queryDataSource, args),
    create: args => self.call_endpoint(@endpoints.createDataSource, args),
    list_templates: args => {
      self.call_endpoint(@endpoints.listDataSourceTemplates, args)
    },
  }
}

///|
pub fn Client::databases(self : Client) -> DatabasesEndpointMethods {
  {
    retrieve: args => self.call_endpoint(@endpoints.getDatabase, args),
    update: args => self.call_endpoint(@endpoints.updateDatabase, args),
    create: args => self.call_endpoint(@endpoints.createDatabase, args),
  }
}

///|
pub fn Client::comments(self : Client) -> CommentsEndpointMethods {
  {
    create: args => self.call_endpoint(@endpoints.createComment, args),
    list: args => self.call_endpoint(@endpoints.listComments, args),
    retrieve: args => self.call_endpoint(@endpoints.getComment, args),
    update: args => self.call_endpoint(@endpoints.updateComment, args),
    delete: args => self.call_endpoint(@endpoints.deleteComment, args),
  }
}

///|
pub fn Client::file_uploads(self : Client) -> FileUploadsEndpointMethods {
  {
    create: args => self.call_endpoint(@endpoints.createFileUpload, args),
    retrieve: args => self.call_endpoint(@endpoints.getFileUpload, args),
    list: args => self.call_endpoint(@endpoints.listFileUploads, args),
    send: args => self.call_endpoint(@endpoints.sendFileUpload, args),
    complete: args => self.call_endpoint(@endpoints.completeFileUpload, args),
  }
}

///|
pub fn Client::views(self : Client) -> ViewsEndpointMethods {
  let create_body : Array[String] = [
    "data_source_id", "name", "type", "database_id", "view_id", "filter", "sorts",
    "quick_filters", "create_database", "configuration", "position", "placement",
  ]
  let update_body : Array[String] = [
    "name", "filter", "sorts", "quick_filters", "configuration",
  ]
  {
    list: args => self.call_endpoint(@endpoints.listDatabaseViews, args),
    create: args => {
      self.call_endpoint_keys(@endpoints.createView, args, create_body, [])
    },
    retrieve: args => self.call_endpoint(@endpoints.getView, args),
    update: args => {
      self.call_endpoint_keys(@endpoints.updateView, args, update_body, [])
    },
    delete: args => self.call_endpoint(@endpoints.deleteView, args),
    queries_create: args => {
      self.call_endpoint_keys(@endpoints.createViewQuery, args, ["page_size"], [])
    },
    queries_results: args => {
      self.call_endpoint(@endpoints.getViewQueryResults, args)
    },
    queries_delete: args => self.call_endpoint(@endpoints.deleteViewQuery, args),
  }
}

///|
pub fn Client::agents(self : Client) -> AgentsEndpointMethods {
  {
    query: args => self.call_endpoint(@endpoints.queryAgents, args),
    retrieve: args => self.call_endpoint(@endpoints.getAgent, args),
    delete: args => self.call_endpoint(@endpoints.deleteAgent, args),
    retrieve_insights: args => self.call_endpoint(@endpoints.getInsights, args),
    update_status: args => {
      self.call_endpoint(@endpoints.updateAgentStatus, args)
    },
    update_credit_limit: args => {
      self.call_endpoint(@endpoints.updateAgentCreditLimit, args)
    },
    batch: args => self.call_endpoint(@endpoints.agentBatch, args),
  }
}

///|
pub fn Client::plugins(self : Client) -> PluginsEndpointMethods {
  {
    list: args => self.call_endpoint(@endpoints.listSkillsPlugins, args),
    // Plugin IDs can be tag IDs containing URL delimiters, not just UUIDs, so
    // the client percent-encodes them before building the path.
    retrieve: args => {
      self.call_endpoint(
        @endpoints.getPluginDirectory,
        encode_string_field(args, "id"),
      )
    },
  }
}

///|
pub fn Client::skills(self : Client) -> SkillsEndpointMethods {
  { retrieve: args => self.call_endpoint(@endpoints.getSkillDirectory, args), }
}

///|
pub fn Client::sessions(self : Client) -> SessionsEndpointMethods {
  {
    retrieve: args => self.call_endpoint(@endpoints.retrieveSession, args),
    update: args => self.call_endpoint(@endpoints.updateSession, args),
    stream: args => {
      let definition = @endpoints.updateSessionStream
      self.warn_unknown_params(args, definition)
      self.stream_request(
        endpoint_request(
          definition,
          args,
          definition.body_params,
          definition.query_params,
        ),
        parse_session_stream_event,
      )
    },
    cancel: args => self.call_endpoint(@endpoints.cancelSession, args),
    query: args => self.call_endpoint(@endpoints.querySessions, args),
    query_events: args => {
      self.call_endpoint(@endpoints.querySessionEvents, args)
    },
  }
}

///|
pub fn Client::oauth(self : Client) -> OauthEndpointMethods {
  {
    token: args => {
      self.call_endpoint_auth(
        @endpoints.oauthToken,
        args,
        oauth_auth_from_args(args),
      )
    },
    introspect: args => {
      self.call_endpoint_auth(
        @endpoints.oauthIntrospect,
        args,
        oauth_auth_from_args(args),
      )
    },
    revoke: args => {
      self.call_endpoint_auth(
        @endpoints.oauthRevoke,
        args,
        oauth_auth_from_args(args),
      )
    },
  }
}

///|
pub fn Client::search(self : Client) -> SearchEndpointMethods {
  { search: args => self.call_endpoint(@endpoints.search, args), }
}

///|
/// Percent-encodes a string field of an arguments object, leaving the object
/// otherwise untouched.
fn encode_string_field(args : Json, name : String) -> Json {
  match args {
    Json::Object(fields) => {
      let out : Map[String, Json] = Map([])
      fields.each((key, value) => {
        if key == name {
          match value {
            Json::String(s) =>
              out.set(key, Json::string(encode_uri_component(s)))
            other => out.set(key, other)
          }
        } else {
          out.set(key, value)
        }
      })
      Json::object(out)
    }
    _ => args
  }
}