from typing import Dict, Optional, Any, List
import time, requests, os
from enum import Enum
from simple_multiprocessing import MultiProcess, Task

from fake_useragent import FakeUserAgent

Response = requests.Response

def download(
    url: str,
    path: str,
    max_request_try_count: int = 3,
    sleep_time: float = 2.5,
    debug: bool = False
) -> bool:
    current_try_count = 0

    while current_try_count < max_request_try_count:
        current_try_count += 1

        if debug:
            print(url + ' | ' + str(current_try_count) + '/' + str(max_request_try_count))

        res = __download(
            url,
            path,
            debug=debug
        )

        if res:
            return True

        time.sleep(sleep_time)

    return False

def __download(
    url: str, 
    path: str,
    debug: bool = False
) -> bool:
    import urllib

    try:
        urllib.request.urlretrieve(url, path)

        return os.path.exists(path)
    except Exception as e1:
        if debug:
            print(e1)
        
        try:
            os.remove(path)
        except Exception as e2:
            if debug:
                print(e2)
        
        return False

def req_multi_download(
    urls_paths: Optional[Dict[str, str]] = None,
    headers: Optional[Dict] = None,
    max_request_try_count: int = 3,
    sleep_time: float = 2.5,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> List[bool]:
    mp = MultiProcess()

    for url, path in urls_paths.items():
        mp.tasks.append(
            Task(
                req_download,
                url,
                path,
                headers=headers,
                max_request_try_count=max_request_try_count,
                user_agent=user_agent,
                sleep_time=sleep_time,
                debug=debug,
                proxy=proxy,
                fake_useragent=fake_useragent,
                proxy_http=proxy_http,
                proxy_https=proxy_https,
                proxy_ftp=proxy_ftp
            )
        )

    return mp.solve()

def req_download(
    url: str,
    path: str,
    headers: Optional[Dict] = None,
    max_request_try_count: int = 3,
    sleep_time: float = 2.5,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> bool:
    current_try_count = 0

    while current_try_count < max_request_try_count:
        current_try_count += 1

        if debug:
            print(url + ' | ' + str(current_try_count) + '/' + str(max_request_try_count))

        res = __req_download(
            url,
            path,
            debug=debug,
            user_agent=user_agent,
            fake_useragent=fake_useragent,
            proxy=proxy,
            proxy_http=proxy_http,
            proxy_https=proxy_https,
            proxy_ftp=proxy_ftp
        )

        if res:
            return True

        time.sleep(sleep_time)

    return False

def __req_download(
    url: str, 
    path: str,
    debug: bool = False,
    headers: Optional[Dict] = None,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> bool:
    headers = headers or {}

    if user_agent or fake_useragent:
        headers = __headers_by_optionally_setting(headers, {'User-Agent':user_agent or FakeUserAgent().random})

    proxies = None

    if proxy or proxy_http or proxy_https or proxy_ftp:
        proxies = {}

        if proxy_http:
            proxies['http'] = proxy_http or proxy

        if proxy_https:
            proxies['https'] = proxy_https or proxy

        if proxy_ftp:
            proxies['ftp'] = proxy_ftp or proxy

    try:
        resp = requests.get(url, headers=headers, proxies=proxies, stream=True)

        if resp and resp.status_code in [200, 201]:
            with open(path, 'wb') as f:
                for chunk in resp.iter_content(1024):
                    f.write(chunk)

                return True
    except Exception as e:
        if debug:
            print(e)

    return False

class RequestMethod(Enum):
    GET     = 'GET'
    POST    = 'POST'
    PUT     = 'PUT'
    DELETE  = 'DELETE'

def request(
    url: str,
    method: RequestMethod = RequestMethod.GET,
    params: Optional[Dict] = None,
    headers: Optional[Dict] = None,
    data: Optional[Any] = None,
    max_request_try_count: int = 10,
    sleep_time: float = 2.5,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> Optional[Response]:
    current_try_count = 0

    while current_try_count < max_request_try_count:
        current_try_count += 1

        if debug:
            print(url + ' | ' + str(current_try_count) + '/' + str(max_request_try_count))

        resp = __request(
            url, method,
            params=params,
            headers=headers,
            data=data,
            debug=debug,
            user_agent=user_agent,
            fake_useragent=fake_useragent,
            proxy=proxy,
            proxy_http=proxy_http,
            proxy_https=proxy_https,
            proxy_ftp=proxy_ftp
        )

        if resp is not None:
            return resp

        if current_try_count >= max_request_try_count:
            break

        time.sleep(sleep_time)

    return None

def get(
    url: str,
    params: Optional[Dict] = None,
    headers: Optional[Dict] = None,
    max_request_try_count: int = 10,
    sleep_time: float = 2.5,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> Optional[Response]:
    return request(url, method=RequestMethod.GET, params=params, headers=headers, max_request_try_count=max_request_try_count, sleep_time=sleep_time, debug=debug, user_agent=user_agent, fake_useragent=fake_useragent, proxy=proxy, proxy_http=proxy_http, proxy_https=proxy_https, proxy_ftp=proxy_ftp)

def post(
    url: str,
    params: Optional[Dict] = None,
    headers: Optional[Dict] = None,
    data: Optional[Any] = None,
    max_request_try_count: int = 10,
    sleep_time: float = 2.5,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> Optional[Response]:
    return request(url, method=RequestMethod.POST, params=params, headers=headers, data=data, max_request_try_count=max_request_try_count, sleep_time=sleep_time, debug=debug, user_agent=user_agent, fake_useragent=fake_useragent, proxy=proxy, proxy_http=proxy_http, proxy_https=proxy_https, proxy_ftp=proxy_ftp)

def put(
    url: str,
    params: Optional[Dict] = None,
    headers: Optional[Dict] = None,
    data: Optional[Any] = None,
    max_request_try_count: int = 10,
    sleep_time: float = 2.5,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> Optional[Response]:
    return request(url, method=RequestMethod.PUT, params=params, headers=headers, data=data, max_request_try_count=max_request_try_count, sleep_time=sleep_time, debug=debug, user_agent=user_agent, fake_useragent=fake_useragent, proxy=proxy, proxy_http=proxy_http, proxy_https=proxy_https, proxy_ftp=proxy_ftp)

def delete(
    url: str,
    params: Optional[Dict] = None,
    headers: Optional[Dict] = None,
    data: Optional[Any] = None,
    max_request_try_count: int = 10,
    sleep_time: float = 2.5,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> Optional[Response]:
    return request(url, method=RequestMethod.DELETE, params=params, headers=headers, data=data, max_request_try_count=max_request_try_count, sleep_time=sleep_time, debug=debug, user_agent=user_agent, fake_useragent=fake_useragent, proxy=proxy, proxy_http=proxy_http, proxy_https=proxy_https, proxy_ftp=proxy_ftp)

def __request(
    url: str,
    method: RequestMethod,
    params: Optional[Dict] = None,
    headers: Optional[Dict] = None,
    data: Optional[Any] = None,
    debug: bool = False,
    user_agent: Optional[str] = None,
    fake_useragent: bool = False,
    proxy: Optional[str] = None,
    proxy_http: Optional[str] = None,
    proxy_https: Optional[str] = None,
    proxy_ftp: Optional[str] = None
) -> Optional[Response]:
    if headers is None:
        headers = {}

    if user_agent or fake_useragent:    
        headers = __headers_by_optionally_setting(
            headers,
            {
                'User-Agent':user_agent or FakeUserAgent().random,
                'Accept':'*/*',
                'Cache-Control':'no-cache',
                'Accept-Encoding':'gzip, deflate, br',
                'Connection':'keep-alive'
            }
        )
    
    if proxy:
        proxy = proxy.lstrip('https://').lstrip('http://').lstrip('ftp://')
    
    if proxy_http or proxy:
        proxy_http = (proxy_http or proxy).lstrip('http://')

    if proxy_https or proxy:
        proxy_https = (proxy_https or proxy).lstrip('https://')

    if proxy_ftp or proxy:
        proxy_ftp = (proxy_ftp or proxy).lstrip('ftp://')

    proxies = None

    if proxy_http or proxy_https or proxy_ftp:
        proxies = {}

        if proxy_http:
            proxies['http'] = 'http://{}'.format(proxy_http)

        if proxy_https:
            proxies['https'] = 'https://{}'.format(proxy_https)

        if proxy_ftp:
            proxies['ftp'] = 'ftp://{}'.format(proxy_ftp)

    try:
        if method == RequestMethod.GET:
            resp = requests.get(url, params=params, headers=headers, proxies=proxies)
        elif method == RequestMethod.POST:
            if type(data) == dict or type(data) == list:
                resp = requests.post(url, json=data, params=params, headers=headers, proxies=proxies)
            else:
                resp = requests.post(url, data=data, params=params, headers=headers, proxies=proxies)
        else:#elif method == RequestMethod.DELETE:
            resp = requests.post(url, data=data, params=params, headers=headers, proxies=proxies)

        if resp is None:
            if debug:
                print('Response is None')
        elif resp.status_code not in [200, 201]:
            if debug:
                print(resp.status_code, resp.text)

            return None

        return resp
    except Exception as e:
        if debug:
            print('ERROR:', e)

        return None

def __headers_by_optionally_setting(
    headers: Dict, 
    keys_values: Dict
) -> Dict:
    for key, value in keys_values.items():
        if key not in headers:
            headers[key] = value
    
    return headers