# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

"""A page that swaps words."""

from __future__ import annotations

from asyncio import Future
from base64 import b64decode, b64encode
from dataclasses import dataclass
from typing import ClassVar, Final

from tornado.web import HTTPError, MissingArgumentError

from .. import DIR as ROOT_DIR
from ..utils.data_parsing import parse_args
from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
from .config_file import InvalidConfigError, SwappedWordsConfig

# the max char count of the text to process
MAX_CHAR_COUNT: Final[int] = 32769 - 1

with (ROOT_DIR / "swapped_words/config.sw").open("r", encoding="UTF-8") as file:
    DEFAULT_CONFIG: Final[SwappedWordsConfig] = SwappedWordsConfig(file.read())


def check_text_too_long(text: str) -> None:
    """Raise an HTTP error if the text is too long."""
    len_text = len(text)

    if len_text > MAX_CHAR_COUNT:
        raise HTTPError(
            413,
            reason=(
                f"The text has {len_text} characters, but it is only allowed "
                f"to have {MAX_CHAR_COUNT} characters. That's "
                f"{len_text - MAX_CHAR_COUNT} characters too much."
            ),
        )


@dataclass(slots=True)
class SwArgs:
    """Arguments used for swapped words."""

    text: str = ""
    config: None | str = None
    reset: bool = False
    return_config: bool = False
    minify_config: bool = False

    def validate(self) -> None:
        """Validate the given data."""
        self.text = self.text.strip()
        check_text_too_long(self.text)
        if self.config:
            self.config = self.config.strip()

    def validate_require_text(self) -> None:
        """Validate the given data and require the text argument."""
        self.validate()
        if not self.text:
            raise MissingArgumentError("text")


class SwappedWords(HTMLRequestHandler):
    """The request handler for the swapped words page."""

    @parse_args(type_=SwArgs, validation_method="validate")
    async def get(self, *, head: bool = False, args: SwArgs) -> None:
        """Handle GET requests to the swapped words page."""
        # pylint: disable=unused-argument
        await self.handle_text(args)

    def handle_text(self, args: SwArgs) -> Future[None]:
        """Use the text to display the HTML page."""
        if args.config is None:
            cookie = self.get_cookie(
                "swapped-words-config",
                None,
            )
            if cookie is not None:
                # decode the base64 text
                args.config = b64decode(cookie).decode("UTF-8")
        else:
            # save the config in a cookie
            self.set_cookie(
                name="swapped-words-config",
                # encode the config as base64
                value=b64encode(args.config.encode("UTF-8")).decode("UTF-8"),
                expires_days=1000,
                path=self.request.path,
                samesite="Strict",
            )

        try:
            sw_config = (
                DEFAULT_CONFIG
                if args.config is None or args.reset
                else SwappedWordsConfig(args.config)
            )
        except InvalidConfigError as exc:
            self.set_status(400)
            return self.render(
                "pages/swapped_words.html",
                text=args.text,
                output="",
                config=args.config,
                MAX_CHAR_COUNT=MAX_CHAR_COUNT,
                error_msg=str(exc),
            )
        # everything went well
        return self.render(
            "pages/swapped_words.html",
            text=args.text,
            output=sw_config.swap_words(args.text),
            config=sw_config.to_config_str(),
            MAX_CHAR_COUNT=MAX_CHAR_COUNT,
            error_msg=None,
        )

    @parse_args(type_=SwArgs, validation_method="validate_require_text")
    async def post(self, *, args: SwArgs) -> None:
        """Handle POST requests to the swapped words page."""
        await self.handle_text(args)


class SwappedWordsAPI(APIRequestHandler):
    """The request handler for the swapped words API."""

    ALLOWED_METHODS: ClassVar[tuple[str, ...]] = ("GET", "POST")

    @parse_args(type_=SwArgs, validation_method="validate")
    async def get(self, *, head: bool = False, args: SwArgs) -> None:
        """Handle GET requests to the swapped words API."""
        # pylint: disable=unused-argument
        try:
            sw_config = (
                DEFAULT_CONFIG
                if args.config is None
                else SwappedWordsConfig(args.config)
            )

            if args.return_config:
                return await self.finish_dict(
                    text=args.text,
                    return_config=True,
                    minify_config=args.minify_config,
                    config=sw_config.to_config_str(args.minify_config),
                    replaced_text=sw_config.swap_words(args.text),
                )
            return await self.finish_dict(
                text=args.text,
                return_config=False,
                replaced_text=sw_config.swap_words(args.text),
            )
        except InvalidConfigError as exc:
            self.set_status(400)
            return await self.finish_dict(
                error=exc.reason,
                line=exc.line,
                line_num=exc.line_num,
            )

    async def post(self) -> None:
        """Handle POST requests to the swapped words API."""
        return await self.get()  # pylint: disable=missing-kwoa
