import logging
import time
from collections.abc import Iterator
from typing import Any
from urllib.parse import urlparse

import requests
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document

log = logging.getLogger(__name__)

MICROSOFT_BROWSE_RETRY_STATUS_CODES = {202, 429, 500, 502, 503, 504}
MICROSOFT_BROWSE_MAX_RETRIES = 2


class MicrosoftWebIQLoader(BaseLoader):
    def __init__(
        self,
        urls: str | list[str],
        api_base_url: str,
        api_key: str,
        language: str = 'en',
        verify_ssl: bool = True,
        timeout: Any = None,
        continue_on_failure: bool = True,
    ) -> None:
        self.urls = urls if isinstance(urls, list) else [urls]
        self.api_base_url = api_base_url.rstrip('/')
        self.api_key = api_key
        self.language = language
        self.verify_ssl = verify_ssl
        self.timeout = timeout
        self.continue_on_failure = continue_on_failure

    def lazy_load(self) -> Iterator[Document]:
        for url in self.urls:
            try:
                doc = self._browse_url(url)
                if doc is not None:
                    yield doc
            except Exception as e:
                if self.continue_on_failure:
                    log.warning(f'Error browsing {url} with Microsoft Web IQ: {e}')
                else:
                    raise e

    def _browse_url(self, url: str) -> Document | None:
        headers = {
            'host': urlparse(self.api_base_url).netloc or 'api.microsoft.ai',
            'x-apikey': self.api_key,
            'content-type': 'application/json',
        }
        payload = {
            'url': url,
            'contentFormat': 'markdown',
            'liveCrawl': 'fallback',
            'renderDynamicPages': True,
            'language': self.language,
        }
        try:
            request_timeout = float(self.timeout)
        except (TypeError, ValueError):
            request_timeout = 60
        request_timeout = request_timeout if request_timeout > 0 else 60

        data: dict[str, Any] = {}
        for attempt in range(MICROSOFT_BROWSE_MAX_RETRIES + 1):
            response = requests.post(
                f'{self.api_base_url}/browse',
                json=payload,
                headers=headers,
                timeout=request_timeout,
                verify=self.verify_ssl,
            )

            if response.status_code in MICROSOFT_BROWSE_RETRY_STATUS_CODES and attempt < MICROSOFT_BROWSE_MAX_RETRIES:
                try:
                    body = response.json()
                except Exception:
                    body = {}
                retry_after = body.get('retryAfter') if isinstance(body, dict) else None
                retry_after = retry_after or response.headers.get('Retry-After')
                try:
                    delay = min(10.0, max(0.0, float(str(retry_after).rstrip('s'))))
                except (TypeError, ValueError):
                    delay = min(8.0, float(2**attempt))
                log.warning(
                    'Microsoft Browse %s returned HTTP %s; retrying in %.1fs',
                    url,
                    response.status_code,
                    delay,
                )
                time.sleep(delay)
                continue

            response.raise_for_status()
            data = response.json()
            break

        content = data.get('content') or ''
        if not isinstance(content, str) or not content.strip():
            return None

        metadata = {'source': data.get('url') or url}
        if data.get('title'):
            metadata['title'] = data['title']

        return Document(page_content=content, metadata=metadata)
