import logging
from .async_client import AsyncNebulaScraper, AsyncClient
import requests
import sys
import ssl
import copyreg

from requests.sessions import Session
from requests.adapters import HTTPAdapter

try:
    from requests_toolbelt.utils import dump as _dump
    _HAS_DUMP = True
except ImportError:
    _HAS_DUMP = False

try:
    import brotli
except ImportError:
    pass

try:
    from urlparse import urlparse
except ImportError:
    from urllib.parse import urlparse

from .exceptions import (
    CloudflareLoopProtection,
    CloudflareIUAMError,
    NebulaScrapeError,
)
from .cloudflare import Cloudflare
from .user_agent import User_Agent
from .session_intel import SessionIntelligence
from .retry_engine import SmartRetryEngine, RetryAction
from .metrics import MetricsCollector
from .fingerprints import get_profile, available_profiles, DEFAULT_PROFILE
from .transports import TransportManager
from .plugins import PluginRegistry

__version__ = '0.0.1'
__author__ = 'MERO'
__contact__ = 'TG@QP4M'


class NebulaScraper(Session):

    def __init__(self, *args, **kwargs):
        self.debug = kwargs.pop('debug', False)

        self.disableCloudflareV1 = kwargs.pop('disableCloudflareV1', False)
        self.delay = kwargs.pop('delay', None)
        self.captcha = kwargs.pop('captcha', {})
        self.doubleDown = kwargs.pop('doubleDown', True)
        self.interpreter = kwargs.pop('interpreter', 'native')

        self.requestPreHook = kwargs.pop('requestPreHook', None)
        self.requestPostHook = kwargs.pop('requestPostHook', None)

        self.cipherSuite = kwargs.pop('cipherSuite', None)
        self.ecdhCurve = kwargs.pop('ecdhCurve', 'prime256v1')
        self.source_address = kwargs.pop('source_address', None)
        self.server_hostname = kwargs.pop('server_hostname', None)
        self.ssl_context = kwargs.pop('ssl_context', None)

        self.auto_retry = kwargs.pop('auto_retry', False)
        self.max_retries = kwargs.pop('max_retries', 5)

        _profile_name = kwargs.pop('profile', DEFAULT_PROFILE)
        _mode = kwargs.pop('mode', 'auto')

        self.allow_brotli = kwargs.pop(
            'allow_brotli',
            True if 'brotli' in sys.modules.keys() else False
        )

        self.user_agent = User_Agent(
            allow_brotli=self.allow_brotli,
            browser=kwargs.pop('browser', None)
        )

        self._solveDepthCnt = 0
        self.solveDepth = kwargs.pop('solveDepth', 3)

        self._intel = SessionIntelligence()
        self._retry_engine = SmartRetryEngine(max_retries=self.max_retries)
        self._metrics = MetricsCollector()
        self._plugins = PluginRegistry()

        super(NebulaScraper, self).__init__(*args, **kwargs)

        if 'requests' in self.headers.get('User-Agent', ''):
            self.headers = self.user_agent.headers
            if not self.cipherSuite:
                self.cipherSuite = self.user_agent.cipherSuite

        if isinstance(self.cipherSuite, list):
            self.cipherSuite = ':'.join(self.cipherSuite)

        try:
            profile_data = get_profile(_profile_name)
        except ValueError:
            profile_data = get_profile(DEFAULT_PROFILE)

        if _profile_name in available_profiles():
            self._profile_name = _profile_name
        else:
            self._profile_name = DEFAULT_PROFILE

        self._transport_manager = TransportManager(profile_name=self._profile_name, mode=_mode)
        self._transport_manager.mount_on(self)

        copyreg.pickle(ssl.SSLContext, lambda obj: (obj.__class__, (obj.protocol,)))

    def __getstate__(self):
        return self.__dict__

    def perform_request(self, method, url, *args, **kwargs):
        return super(NebulaScraper, self).request(method, url, *args, **kwargs)

    def simpleException(self, exception, msg):
        self._solveDepthCnt = 0
        sys.tracebacklimit = 0
        raise exception(msg)

    @staticmethod
    def debugRequest(req):
        if _HAS_DUMP:
            try:
                print(_dump.dump_all(req).decode('utf-8', errors='backslashreplace'))
            except ValueError as e:
                print(f"Debug Error: {getattr(e, 'message', e)}")

    def decodeBrotli(self, resp):
        if requests.packages.urllib3.__version__ < '1.25.1' and resp.headers.get('Content-Encoding') == 'br':
            if self.allow_brotli and resp._content:
                resp._content = brotli.decompress(resp.content)
            else:
                logging.warning(
                    f'urllib3 {requests.packages.urllib3.__version__} — Brotli content detected '
                    'but allow_brotli=False — skipping decompress.'
                )
        return resp

    def _attach_meta(self, response, intel_result=None, metrics=None):
        if not hasattr(response, 'meta'):
            response.meta = {}
        if intel_result is not None:
            response.meta.update(intel_result.to_meta())
        if metrics is not None:
            response.meta['metrics'] = {
                'latency_ms': metrics.latency_ms,
                'tls_handshake_ms': metrics.tls_handshake_ms,
                'retry_count': metrics.retry_count,
                'redirect_depth': metrics.redirect_depth,
                'transport_used': metrics.transport_used,
            }
        return response

    def register_plugin(self, plugin):
        self._plugins.register(plugin)

    def request(self, method, url, *args, **kwargs):
        if kwargs.get('proxies') and kwargs.get('proxies') != self.proxies:
            self.proxies = kwargs.get('proxies')

        kwargs = self._plugins.run_pre_request(self, method, url, kwargs)

        if self.requestPreHook:
            (method, url, args, kwargs) = self.requestPreHook(self, method, url, *args, **kwargs)

        self._metrics.start(transport=self._transport_manager.current_name)
        self._retry_engine.reset()

        response = None
        last_intel = None

        while True:
            try:
                response = self.decodeBrotli(self.perform_request(method, url, *args, **kwargs))
                self._metrics.mark_connected()
            except Exception as e:
                if self.auto_retry and self._retry_engine.attempt < self.max_retries:
                    self._retry_engine._attempt += 1
                    self._transport_manager.rebuild(self)
                    continue
                raise

            if self.debug:
                self.debugRequest(response)

            last_intel = self._intel.analyze(response)

            if not self.disableCloudflareV1:
                cf_handler = Cloudflare(self)
                if cf_handler.is_Challenge_Request(response):
                    if self._solveDepthCnt >= self.solveDepth:
                        _ = self._solveDepthCnt
                        self.simpleException(
                            CloudflareLoopProtection,
                            f"Loop Protection — solved {_} times in a row."
                        )
                    self._solveDepthCnt += 1
                    self._metrics.set_challenge_solved()
                    response = cf_handler.Challenge_Response(response, **kwargs)
                    last_intel = self._intel.analyze(response)
                else:
                    if not response.is_redirect and response.status_code not in [429, 503]:
                        self._solveDepthCnt = 0

            if self.auto_retry:
                decision = self._retry_engine.analyze_response(response, last_intel)
                self._plugins.run_retry(self, self._retry_engine.attempt, decision)

                if decision.action.value == 'pass':
                    self._transport_manager.record_success()
                    break

                if decision.action.value == 'abort':
                    break

                if decision.rotate_session:
                    try:
                        new_ua = User_Agent(
                            allow_brotli=self.allow_brotli,
                            browser=None
                        )
                        self.headers.update(new_ua.headers)
                    except Exception:
                        pass

                if decision.rebuild_connection:
                    self._transport_manager.rebuild(self)
                elif decision.switch_transport:
                    self._transport_manager.escalate(self)

                self._metrics.increment_retry()
                self._retry_engine.execute_decision(decision)
            else:
                break

        if self.requestPostHook:
            newResponse = self.requestPostHook(self, response)
            if response != newResponse:
                response = newResponse
                if self.debug:
                    print('==== requestPostHook Debug ====')
                    self.debugRequest(response)

        response = self._plugins.run_post_request(self, response, kwargs)

        finished = self._metrics.finish(response)
        self._attach_meta(response, last_intel, finished)

        return response

    @classmethod
    def create_scraper(cls, sess=None, **kwargs):
        scraper = cls(**kwargs)
        if sess:
            for attr in ['auth', 'cert', 'cookies', 'headers', 'hooks', 'params', 'proxies', 'data']:
                val = getattr(sess, attr, None)
                if val is not None:
                    setattr(scraper, attr, val)
        return scraper

    @classmethod
    def get_tokens(cls, url, **kwargs):
        scraper = cls.create_scraper(
            **{
                field: kwargs.pop(field, None) for field in [
                    'allow_brotli', 'browser', 'debug', 'delay',
                    'doubleDown', 'captcha', 'interpreter',
                    'source_address', 'requestPreHook', 'requestPostHook',
                    'profile', 'auto_retry',
                ] if field in kwargs
            }
        )
        try:
            resp = scraper.get(url, **kwargs)
            resp.raise_for_status()
        except Exception:
            logging.error(f'"{url}" returned an error. Could not collect tokens.')
            raise

        domain = urlparse(resp.url).netloc
        cookie_domain = None

        for d in scraper.cookies.list_domains():
            if d.startswith('.') and d in (f'.{domain}'):
                cookie_domain = d
                break
        else:
            cls.simpleException(
                cls,
                CloudflareIUAMError,
                "Unable to find Cloudflare cookies."
            )

        return (
            {'cf_clearance': scraper.cookies.get('cf_clearance', '', domain=cookie_domain)},
            scraper.headers['User-Agent']
        )

    @classmethod
    def get_cookie_string(cls, url, **kwargs):
        tokens, user_agent = cls.get_tokens(url, **kwargs)
        return '; '.join('='.join(pair) for pair in tokens.items()), user_agent


class Client:
    def __init__(
        self,
        profile: str = DEFAULT_PROFILE,
        auto_retry: bool = True,
        max_retries: int = 5,
        mode: str = "auto",
        captcha: dict = None,
        interpreter: str = "native",
        debug: bool = False,
        **kwargs
    ):
        self._scraper = NebulaScraper(
            profile=profile,
            auto_retry=auto_retry,
            max_retries=max_retries,
            mode=mode,
            captcha=captcha or {},
            interpreter=interpreter,
            debug=debug,
            **kwargs
        )

    def get(self, url, **kwargs):
        return self._scraper.get(url, **kwargs)

    def post(self, url, **kwargs):
        return self._scraper.post(url, **kwargs)

    def put(self, url, **kwargs):
        return self._scraper.put(url, **kwargs)

    def delete(self, url, **kwargs):
        return self._scraper.delete(url, **kwargs)

    def request(self, method, url, **kwargs):
        return self._scraper.request(method, url, **kwargs)

    def register_plugin(self, plugin):
        self._scraper.register_plugin(plugin)

    @property
    def metrics(self):
        return self._scraper._metrics.session_metrics()

    @property
    def session(self) -> NebulaScraper:
        return self._scraper


if ssl.OPENSSL_VERSION_INFO < (1, 1, 1):
    print(
        f"WARNING: OpenSSL {ssl.OPENSSL_VERSION} does not support TLS 1.3 — "
        "you may encounter Cloudflare blocks."
    )

create_scraper = NebulaScraper.create_scraper
session = NebulaScraper.create_scraper
get_tokens = NebulaScraper.get_tokens
get_cookie_string = NebulaScraper.get_cookie_string
SmartScraper = Client

__all__ = [
    'NebulaScraper',
    'Client',
    'AsyncNebulaScraper',
    'AsyncClient',
    'SmartScraper',
    'create_scraper',
    'session',
    'get_tokens',
    'get_cookie_string',
    '__version__',
    '__author__',
    '__contact__',
]
