from __future__ import annotations

import logging
import time
from enum import Enum
from typing import Any, Mapping

import requests

from loki.common.profile import Profile

logger = logging.getLogger("loki")


class Method(str, Enum):
    GET = "GET"
    POST = "POST"
    PUT = "PUT"
    PATCH = "PATCH"
    DELETE = "DELETE"
    HEAD = "HEAD"
    OPTIONS = "OPTIONS"


class RequestMixin:
    def __init__(
        self,
        *,
        session: requests.Session | None = None,
        headers: Mapping[str, str] | None = None,
        profile: Profile | None = None,
        auto_load_cookies: bool = True,
    ) -> None:
        self.session = session or requests.Session()
        self.profile = profile
        self.headers: dict[str, str] = dict(headers or {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36'
        })
        if self.profile and auto_load_cookies:
            self.profile.load_cookies_into(self.session)

    def request(
        self,
        method: Method | str,
        url: str,
        *,
        params: Mapping[str, Any] | None = None,
        headers: Mapping[str, str] | None = None,
        data: Any | None = None,
        json: Any | None = None,
        files: Mapping[str, Any] | None = None,
        max_retries: int = 3,
        backoff_factor: float = 0.5,
        timeout: float = 10,
    ) -> requests.Response | None:
        method_str = method.value if isinstance(method, Method) else str(method)
        merged_headers = {**self.headers, **(dict(headers) if headers else {})}

        for attempt in range(1, max_retries + 1):
            try:
                resp = self.session.request(
                    method_str,
                    url,
                    params=dict(params) if params else None,
                    headers=merged_headers or None,
                    data=data,
                    json=json,
                    files=dict(files) if files else None,
                    timeout=timeout,
                )

                if resp.status_code < 500:
                    return resp

                logger.warning(
                    "Request %s failed with status %s, attempt %s/%s",
                    url,
                    resp.status_code,
                    attempt,
                    max_retries,
                )
            except (requests.Timeout, requests.ConnectionError) as err:
                logger.warning(
                    "Request %s exception %s, attempt %s/%s",
                    url,
                    err,
                    attempt,
                    max_retries,
                )
            except Exception as err:
                logger.exception("Request %s fatal error: %s", url, err)
                return None

            if attempt < max_retries:
                time.sleep(backoff_factor * (2 ** (attempt - 1)))

        return None

    def save_cookies(self) -> None:
        if self.profile:
            self.profile.save_cookies_from(self.session)

    def close(self, *, save_cookies: bool = True) -> None:
        if save_cookies:
            self.save_cookies()
        self.session.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        self.close(save_cookies=True)
        return False

    # Backwards compat with earlier scrapers naming.
    _request = request
