# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""URL manipulation utilities."""

import re
from collections.abc import Sequence
from typing import Literal, Self, overload, override

# (connect, read) timeout for external HTTP requests
DEFAULT_REQUEST_TIMEOUT = (5, 30)

re_url_path_sep = re.compile(r"/+")


class URLPath(Sequence[str]):
    """
    Store and manipulate the path component of a URL.

    It behaves as a list of split path components.
    """

    #: True if the path refers to a file, False if it refers to a directory
    is_file: bool

    def __init__(
        self,
        path: str = "",
        is_file: bool | Literal["autodetect"] = "autodetect",
    ) -> None:
        """
        Construct a URLPath from a path string.

        :param path: the path component of a parsed URL
        :param is_file: if the path refers to a file or a directory. If unset,
          it will be autodetected by checking if path ends with a slash

        Multiple consecutive slashes in the path are treated like a single
        slash instead of empty path components.

        Leading slashes in path are ignored.
        """
        stripped_path = path.strip("/")
        self.path = (
            re_url_path_sep.split(stripped_path) if stripped_path else []
        )
        if is_file == "autodetect":
            self.is_file = bool(stripped_path) and not path.endswith("/")
        else:
            self.is_file = is_file

    @override
    def __str__(self) -> str:
        """
        Return the path as a string.

        Paths referring to a directory will end with a trailing slash.
        """
        res = "/".join(self.path)
        if not self.is_file:
            res += "/"
        return res

    @override
    def __len__(self) -> int:
        """Return the number of path components."""
        return len(self.path)

    @overload
    def __getitem__(self, idx: int, /) -> str: ...  # noqa: D105

    @overload
    def __getitem__(self, idx: slice, /) -> Sequence[str]: ...  # noqa: D105

    @override
    def __getitem__(self, idx: int | slice, /) -> str | Sequence[str]:
        """Return a path component by its list index."""
        return self.path[idx]

    def __add__(self, value: "URLPath | str") -> "URLPath":
        """
        Append two URLPath objects.

        If the first URLPath is a file, the file component is removed.

        Adding a string will automatically convert it to a URLPath, with
        is_file autodetected from the presence of a trailing slash.
        """
        match value:
            case URLPath():
                pass
            case str():
                value = URLPath(value)
            case _:
                return NotImplemented
        res = URLPath()
        if self.is_file:
            res.path += self.path[:-1]
        else:
            res.path += self.path
        res.path += value.path
        res.is_file = value.is_file
        return res

    def __iadd__(self, value: "URLPath | str") -> Self:
        """
        In-place append.

        If this URLPath is a file, the file component is removed.

        Adding a string will automatically convert it to a URLPath, with
        is_file autodetected from the presence of a trailing slash.
        """
        match value:
            case URLPath():
                pass
            case str():
                value = URLPath(value)
            case _:
                return NotImplemented
        if self.is_file:
            self.pop()
        self.path += value.path
        self.is_file = value.is_file
        return self

    def pop(self) -> None:
        """Remove the last element from the path."""
        self.is_file = False
        if self.path:
            self.path.pop()


def parse_timeout(raw_timeout: str) -> int | tuple[int, int]:
    """Parse a string into an integer or a tuple of two integers."""
    try:
        if "," in raw_timeout:
            as_ints = tuple(int(x.strip()) for x in raw_timeout.split(",", 1))
            return (as_ints[0], as_ints[1])
        return int(raw_timeout)
    except (ValueError, IndexError):
        raise ValueError(f"Invalid timeout value: {raw_timeout}")
