# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ApiSpec.StatusOperation do
  alias OpenApiSpex.Operation
  alias OpenApiSpex.Schema
  alias Pleroma.Web.ApiSpec.AccountOperation
  alias Pleroma.Web.ApiSpec.Schemas.Account
  alias Pleroma.Web.ApiSpec.Schemas.ApiError
  alias Pleroma.Web.ApiSpec.Schemas.Attachment
  alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
  alias Pleroma.Web.ApiSpec.Schemas.Emoji
  alias Pleroma.Web.ApiSpec.Schemas.FlakeID
  alias Pleroma.Web.ApiSpec.Schemas.Poll
  alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
  alias Pleroma.Web.ApiSpec.Schemas.Status
  alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope

  import Pleroma.Web.ApiSpec.Helpers

  def open_api_operation(action) do
    operation = String.to_existing_atom("#{action}_operation")
    apply(__MODULE__, operation, [])
  end

  def index_operation do
    %Operation{
      tags: ["Retrieve status information"],
      summary: "Multiple statuses",
      security: [%{"oAuth" => ["read:statuses"]}],
      parameters: [
        Operation.parameter(
          :ids,
          :query,
          %Schema{type: :array, items: FlakeID},
          "Array of status IDs"
        ),
        Operation.parameter(
          :with_muted,
          :query,
          BooleanLike,
          "Include reactions from muted acccounts."
        )
      ],
      operationId: "StatusController.index",
      responses: %{
        200 => Operation.response("Array of Status", "application/json", array_of_statuses())
      }
    }
  end

  def create_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Publish new status",
      security: [%{"oAuth" => ["write:statuses"]}],
      description: "Post a new status",
      operationId: "StatusController.create",
      requestBody: request_body("Parameters", create_request(), required: true),
      responses: %{
        200 =>
          Operation.response(
            "Status. When `scheduled_at` is present, ScheduledStatus is returned instead",
            "application/json",
            %Schema{anyOf: [Status, ScheduledStatus]}
          ),
        422 => Operation.response("Bad Request / MRF Rejection", "application/json", ApiError)
      }
    }
  end

  def show_operation do
    %Operation{
      tags: ["Retrieve status information"],
      summary: "Status",
      description: "View information about a status",
      operationId: "StatusController.show",
      security: [%{"oAuth" => ["read:statuses"]}],
      parameters: [
        id_param(),
        Operation.parameter(
          :with_muted,
          :query,
          BooleanLike,
          "Include reactions from muted acccounts."
        )
      ],
      responses: %{
        200 => status_response(),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def delete_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Delete",
      security: [%{"oAuth" => ["write:statuses"]}],
      description: "Delete one of your own statuses",
      operationId: "StatusController.delete",
      parameters: [id_param()],
      responses: %{
        200 => status_response(),
        403 => Operation.response("Forbidden", "application/json", ApiError),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def reblog_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Reblog",
      security: [%{"oAuth" => ["write:statuses"]}],
      description: "Share a status",
      operationId: "StatusController.reblog",
      parameters: [id_param()],
      requestBody:
        request_body("Parameters", %Schema{
          type: :object,
          properties: %{
            visibility: %Schema{allOf: [VisibilityScope]}
          }
        }),
      responses: %{
        200 => status_response(),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def unreblog_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Undo reblog",
      security: [%{"oAuth" => ["write:statuses"]}],
      description: "Undo a reshare of a status",
      operationId: "StatusController.unreblog",
      parameters: [id_param()],
      responses: %{
        200 => status_response(),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def favourite_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Favourite",
      security: [%{"oAuth" => ["write:favourites"]}],
      description: "Add a status to your favourites list",
      operationId: "StatusController.favourite",
      parameters: [id_param()],
      responses: %{
        200 => status_response(),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def unfavourite_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Undo favourite",
      security: [%{"oAuth" => ["write:favourites"]}],
      description: "Remove a status from your favourites list",
      operationId: "StatusController.unfavourite",
      parameters: [id_param()],
      responses: %{
        200 => status_response(),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def pin_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Pin to profile",
      security: [%{"oAuth" => ["write:accounts"]}],
      description: "Feature one of your own public statuses at the top of your profile",
      operationId: "StatusController.pin",
      parameters: [id_param()],
      responses: %{
        200 => status_response(),
        400 =>
          Operation.response("Bad Request", "application/json", %Schema{
            allOf: [ApiError],
            title: "Unprocessable Entity",
            example: %{
              "error" => "You have already pinned the maximum number of statuses"
            }
          }),
        404 =>
          Operation.response("Not found", "application/json", %Schema{
            allOf: [ApiError],
            title: "Unprocessable Entity",
            example: %{
              "error" => "Record not found"
            }
          }),
        422 =>
          Operation.response(
            "Unprocessable Entity",
            "application/json",
            %Schema{
              allOf: [ApiError],
              title: "Unprocessable Entity",
              example: %{
                "error" => "Someone else's status cannot be pinned"
              }
            }
          )
      }
    }
  end

  def unpin_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Unpin from profile",
      security: [%{"oAuth" => ["write:accounts"]}],
      description: "Unfeature a status from the top of your profile",
      operationId: "StatusController.unpin",
      parameters: [id_param()],
      responses: %{
        200 => status_response(),
        400 =>
          Operation.response("Bad Request", "application/json", %Schema{
            allOf: [ApiError],
            title: "Unprocessable Entity",
            example: %{
              "error" => "You have already pinned the maximum number of statuses"
            }
          }),
        404 =>
          Operation.response("Not found", "application/json", %Schema{
            allOf: [ApiError],
            title: "Unprocessable Entity",
            example: %{
              "error" => "Record not found"
            }
          })
      }
    }
  end

  def bookmark_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Bookmark",
      security: [%{"oAuth" => ["write:bookmarks"]}],
      description: "Privately bookmark a status",
      operationId: "StatusController.bookmark",
      parameters: [id_param()],
      responses: %{
        200 => status_response()
      }
    }
  end

  def unbookmark_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Undo bookmark",
      security: [%{"oAuth" => ["write:bookmarks"]}],
      description: "Remove a status from your private bookmarks",
      operationId: "StatusController.unbookmark",
      parameters: [id_param()],
      responses: %{
        200 => status_response()
      }
    }
  end

  def mute_conversation_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Mute conversation",
      security: [%{"oAuth" => ["write:mutes"]}],
      description: "Do not receive notifications for the thread that this status is part of.",
      operationId: "StatusController.mute_conversation",
      requestBody:
        request_body("Parameters", %Schema{
          type: :object,
          properties: %{
            expires_in: %Schema{
              type: :integer,
              nullable: true,
              description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
              default: 0
            }
          }
        }),
      parameters: [
        id_param(),
        Operation.parameter(
          :expires_in,
          :query,
          %Schema{type: :integer, default: 0},
          "Expire the mute in `expires_in` seconds. Default 0 for infinity"
        )
      ],
      responses: %{
        200 => status_response(),
        400 => Operation.response("Error", "application/json", ApiError)
      }
    }
  end

  def unmute_conversation_operation do
    %Operation{
      tags: ["Status actions"],
      summary: "Unmute conversation",
      security: [%{"oAuth" => ["write:mutes"]}],
      description:
        "Start receiving notifications again for the thread that this status is part of",
      operationId: "StatusController.unmute_conversation",
      parameters: [id_param()],
      responses: %{
        200 => status_response(),
        400 => Operation.response("Error", "application/json", ApiError)
      }
    }
  end

  def favourited_by_operation do
    %Operation{
      tags: ["Retrieve status information"],
      summary: "Favourited by",
      description: "View who favourited a given status",
      operationId: "StatusController.favourited_by",
      security: [%{"oAuth" => ["read:accounts"]}],
      parameters: [id_param()],
      responses: %{
        200 =>
          Operation.response(
            "Array of Accounts",
            "application/json",
            AccountOperation.array_of_accounts()
          ),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def reblogged_by_operation do
    %Operation{
      tags: ["Retrieve status information"],
      summary: "Reblogged by",
      description: "View who reblogged a given status",
      operationId: "StatusController.reblogged_by",
      security: [%{"oAuth" => ["read:accounts"]}],
      parameters: [id_param()],
      responses: %{
        200 =>
          Operation.response(
            "Array of Accounts",
            "application/json",
            AccountOperation.array_of_accounts()
          ),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def context_operation do
    %Operation{
      tags: ["Retrieve status information"],
      summary: "Parent and child statuses",
      description: "View statuses above and below this status in the thread",
      operationId: "StatusController.context",
      security: [%{"oAuth" => ["read:statuses"]}],
      parameters: [id_param()],
      responses: %{
        200 => Operation.response("Context", "application/json", context())
      }
    }
  end

  def favourites_operation do
    %Operation{
      tags: ["Timelines"],
      summary: "Favourited statuses",
      description:
        "Statuses the user has favourited. Please note that you have to use the link headers to paginate this. You can not build the query parameters yourself.",
      operationId: "StatusController.favourites",
      parameters: pagination_params(),
      security: [%{"oAuth" => ["read:favourites"]}],
      responses: %{
        200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
      }
    }
  end

  def bookmarks_operation do
    %Operation{
      tags: ["Timelines"],
      summary: "Bookmarked statuses",
      description: "Statuses the user has bookmarked",
      operationId: "StatusController.bookmarks",
      parameters: pagination_params(),
      security: [%{"oAuth" => ["read:bookmarks"]}],
      responses: %{
        200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
      }
    }
  end

  def translate_operation do
    %Operation{
      tags: ["Retrieve status translation"],
      summary: "Translate status",
      description: "View the translation of a given status",
      operationId: "StatusController.translation",
      security: [%{"oAuth" => ["read:statuses"]}],
      parameters: [id_param(), language_param(), source_language_param()],
      responses: %{
        200 => Operation.response("Translation", "application/json", translation()),
        400 => Operation.response("Error", "application/json", ApiError),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def show_history_operation do
    %Operation{
      tags: ["Retrieve status history"],
      summary: "Status history",
      description: "View history of a status",
      operationId: "StatusController.show_history",
      security: [%{"oAuth" => ["read:statuses"]}],
      parameters: [
        id_param()
      ],
      responses: %{
        200 => status_history_response(),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def show_source_operation do
    %Operation{
      tags: ["Retrieve status source"],
      summary: "Status source",
      description: "View source of a status",
      operationId: "StatusController.show_source",
      security: [%{"oAuth" => ["read:statuses"]}],
      parameters: [
        id_param()
      ],
      responses: %{
        200 => status_source_response(),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def update_operation do
    %Operation{
      tags: ["Update status"],
      summary: "Update status",
      description: "Change the content of a status",
      operationId: "StatusController.update",
      security: [%{"oAuth" => ["write:statuses"]}],
      parameters: [
        id_param()
      ],
      requestBody: request_body("Parameters", update_request(), required: true),
      responses: %{
        200 => status_response(),
        403 => Operation.response("Forbidden", "application/json", ApiError),
        404 => Operation.response("Not Found", "application/json", ApiError)
      }
    }
  end

  def array_of_statuses do
    %Schema{type: :array, items: Status, example: [Status.schema().example]}
  end

  defp create_request do
    %Schema{
      title: "StatusCreateRequest",
      type: :object,
      properties: %{
        status: %Schema{
          type: :string,
          nullable: true,
          description:
            "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
        },
        media_ids: %Schema{
          nullable: true,
          type: :array,
          items: %Schema{type: :string},
          description: "Array of Attachment ids to be attached as media."
        },
        poll: poll_params(),
        in_reply_to_id: %Schema{
          nullable: true,
          allOf: [FlakeID],
          description: "ID of the status being replied to, if status is a reply"
        },
        sensitive: %Schema{
          allOf: [BooleanLike],
          nullable: true,
          description: "Mark status and attached media as sensitive?"
        },
        spoiler_text: %Schema{
          type: :string,
          nullable: true,
          description:
            "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
        },
        scheduled_at: %Schema{
          type: :string,
          format: :"date-time",
          nullable: true,
          description:
            "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
        },
        language: %Schema{
          type: :string,
          nullable: true,
          description: "ISO 639 language code for this status."
        },
        # Pleroma-specific properties:
        preview: %Schema{
          allOf: [BooleanLike],
          nullable: true,
          description:
            "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
        },
        content_type: %Schema{
          type: :string,
          nullable: true,
          description:
            "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
        },
        to: %Schema{
          type: :array,
          nullable: true,
          items: %Schema{type: :string},
          description:
            "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
        },
        visibility: %Schema{
          nullable: true,
          anyOf: [
            VisibilityScope,
            %Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
          ],
          description:
            "Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
        },
        expires_in: %Schema{
          nullable: true,
          type: :integer,
          description:
            "The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour."
        },
        in_reply_to_conversation_id: %Schema{
          nullable: true,
          type: :string,
          description:
            "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
        },
        quote_id: %Schema{
          nullable: true,
          type: :string,
          description: "Will quote a given status."
        }
      },
      example: %{
        "status" => "What time is it?",
        "sensitive" => "false",
        "poll" => %{
          "options" => ["Cofe", "Adventure"],
          "expires_in" => 420
        }
      }
    }
  end

  defp update_request do
    %Schema{
      title: "StatusUpdateRequest",
      type: :object,
      properties: %{
        status: %Schema{
          type: :string,
          nullable: true,
          description:
            "Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
        },
        media_ids: %Schema{
          nullable: true,
          type: :array,
          items: %Schema{type: :string},
          description: "Array of Attachment ids to be attached as media."
        },
        poll: poll_params(),
        sensitive: %Schema{
          allOf: [BooleanLike],
          nullable: true,
          description: "Mark status and attached media as sensitive?"
        },
        spoiler_text: %Schema{
          type: :string,
          nullable: true,
          description:
            "Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
        },
        content_type: %Schema{
          type: :string,
          nullable: true,
          description:
            "The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
        },
        to: %Schema{
          type: :array,
          nullable: true,
          items: %Schema{type: :string},
          description:
            "A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
        }
      },
      example: %{
        "status" => "What time is it?",
        "sensitive" => "false",
        "poll" => %{
          "options" => ["Cofe", "Adventure"],
          "expires_in" => 420
        }
      }
    }
  end

  def poll_params do
    %Schema{
      nullable: true,
      type: :object,
      required: [:options, :expires_in],
      properties: %{
        options: %Schema{
          type: :array,
          items: %Schema{type: :string},
          description: "Array of possible answers. Must be provided with `poll[expires_in]`."
        },
        expires_in: %Schema{
          type: :integer,
          nullable: true,
          description:
            "Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
        },
        multiple: %Schema{
          allOf: [BooleanLike],
          nullable: true,
          description: "Allow multiple choices?"
        },
        hide_totals: %Schema{
          allOf: [BooleanLike],
          nullable: true,
          description: "Hide vote counts until the poll ends?"
        }
      }
    }
  end

  def id_param do
    Operation.parameter(:id, :path, FlakeID, "Status ID",
      example: "9umDrYheeY451cQnEe",
      required: true
    )
  end

  defp language_param do
    Operation.parameter(:language, :path, :string, "ISO 639 language code", example: "en")
  end

  defp source_language_param do
    Operation.parameter(:from, :query, :string, "ISO 639 language code", example: "en")
  end

  defp status_response do
    Operation.response("Status", "application/json", Status)
  end

  defp status_history_response do
    Operation.response(
      "Status History",
      "application/json",
      %Schema{
        title: "Status history",
        description: "Response schema for history of a status",
        type: :array,
        items: %Schema{
          type: :object,
          properties: %{
            account: %Schema{
              allOf: [Account],
              description: "The account that authored this status"
            },
            content: %Schema{
              type: :string,
              format: :html,
              description: "HTML-encoded status content"
            },
            sensitive: %Schema{
              type: :boolean,
              description: "Is this status marked as sensitive content?"
            },
            spoiler_text: %Schema{
              type: :string,
              description:
                "Subject or summary line, below which status content is collapsed until expanded"
            },
            created_at: %Schema{
              type: :string,
              format: "date-time",
              description: "The date when this status was created"
            },
            media_attachments: %Schema{
              type: :array,
              items: Attachment,
              description: "Media that is attached to this status"
            },
            emojis: %Schema{
              type: :array,
              items: Emoji,
              description: "Custom emoji to be used when rendering status content"
            },
            poll: %Schema{
              allOf: [Poll],
              nullable: true,
              description: "The poll attached to the status"
            }
          }
        }
      }
    )
  end

  defp status_source_response do
    Operation.response(
      "Status Source",
      "application/json",
      %Schema{
        type: :object,
        properties: %{
          id: FlakeID,
          text: %Schema{
            type: :string,
            description: "Raw source of status content"
          },
          spoiler_text: %Schema{
            type: :string,
            description:
              "Subject or summary line, below which status content is collapsed until expanded"
          },
          content_type: %Schema{
            type: :string,
            description: "The content type of the source"
          }
        }
      }
    )
  end

  defp context do
    %Schema{
      title: "StatusContext",
      description:
        "Represents the tree around a given status. Used for reconstructing threads of statuses.",
      type: :object,
      required: [:ancestors, :descendants],
      properties: %{
        ancestors: array_of_statuses(),
        descendants: array_of_statuses()
      },
      example: %{
        "ancestors" => [Status.schema().example],
        "descendants" => [Status.schema().example]
      }
    }
  end

  defp translation do
    %Schema{
      title: "StatusTranslation",
      description: "The translation of a status.",
      type: :object,
      required: [:detected_language, :text],
      properties: %{
        detected_language: %Schema{
          type: :string,
          description: "The detected language of the text"
        },
        text: %Schema{type: :string, description: "The translated text"}
      }
    }
  end
end