# -*- coding:utf-8 -*-
# encoding=utf-8
# @modified 2021/11/28 19:47:17
# decode: bytes -> str
# encode: str -> bytes
"""
@Author       : xupingmao
@email        : 578749341@qq.com
@Date         : 2021/11/28 19:47:17
@LastEditors  : xupingmao
@LastEditTime : 2024-08-03 10:36:24
@FilePath     : /xnote/xutils/netutil.py
"""

import os
import re
import codecs
import xutils.six as six
import socket
import io
import gzip
import logging
import http.client
import typing

from typing import Optional
from urllib.parse import parse_qs
from xutils.imports import try_decode
from xutils.base import print_exc
from urllib.parse import quote

# TODO fix SSLV3_ALERT_HANDSHAKE_FAILURE on MacOS
from http.client import HTTPConnection
from urllib.request import urlopen, Request

try:
    import requests
except ImportError:
    requests = None

BUFSIZE = 1024 * 512

USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36"

class NetworkMock:
    def http_get(self, url: str, charset, params):
        raise NotImplementedError("http_get")
    def http_post(self, url: str, body:str, charset:str):
        raise NotImplementedError("http_post")

_mock : typing.Optional[NetworkMock] = None

def set_net_mock(mock):
    """设置单元测试mock"""
    global _mock
    _mock = mock

def splithost(url):
    """
        >>> splithost('//host[:port]/path')
        ('host[:port]', '/path')
        >>> splithost('http://www.baidu.com/index.html')
        ('www.baidu.com', '/index.html')
    """
    pattern = re.compile('^(http:|https:)?(//)?([^/?]*)(.*)$')
    match = pattern.match(url)
    if match: return match.group(3, 4)
    return None, url

def join_web_path(web_root: str, webpath: str):
    """连接网络路径"""
    if len(webpath) > 0 and webpath[0] == "/":
        webpath = webpath[1:]
    if os.name == "nt":
        webpath = webpath.replace("/", "\\")
    return os.path.join(web_root, webpath)


def get_http_home(host: str):
    """
    >>> get_http_home("www.xnote.com")
    'http://www.xnote.com'
    >>> get_http_home("https://www.xnote.com")
    'https://www.xnote.com'
    """
    assert host != None
    if not host.startswith(("http://", "https://")):
        return "http://" + host
    return host

def get_http_url(url: str):
    if not url.startswith(("http://", "https://")):
        url = "http://" + url
    return url.split("#")[0]

def is_http_url(url: str):
    if not isinstance(url, str):
        return False

    if not url.startswith(("http://", "https://")):
        return False
    
    return True

def get_host_by_url(url: str):
    """
        >>> get_host("http://www.baidu.com/index.html")
        'www.baidu.com'
    """
    p = r"https?://([^\/]+)"
    m = re.match(p, url)
    if m and m.groups():
        return m.groups()[0]
    return None

def get_host(url):
    return get_host_by_url(url)


def get_local_ip_by_socket():
    """使用UDP协议获取当前的局域网地址
    """
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.connect(("128.0.0.1", 80)) # 随便一个有效的IP地址即可
        host, port = s.getsockname()
        return host


class HttpResource:

    def __init__(self, url: str):
        self.url = get_http_url(url)
        self.protocol = url.split("://")[0]
        self.domain: str = splithost(url)[0]

    def get_res_url(self, url: str):
        """
            >>> HttpResource("http://www.a.com").get_res_url("https://b.com/b.png")
            'https://b.com/b.png'
            >>> HttpResource("http://www.a.com").get_res_url("//b.com/b.png")
            'http://b.com/b.png'
            >>> HttpResource("http://www.a.com").get_res_url("/b.png")
            'http://www.a.com/b.png'
            >>> HttpResource("http://www.a.com/a").get_res_url("b.png")
            'http://www.a.com/a/b.png'
        """
        if not url:
            return None
        if url.startswith(("http://", "https://")):
            return url
        if url.startswith("//"):
            return "http:" + url
        if url[0] == '/':
            # 绝对路径
            return self.protocol + "://" + self.domain + url
        # 相对路径
        return self.url.rstrip("/") + "/" + url

    def get_res(self, url):
        return HttpResource(self.get_res_url(url))

    def get(self, charset = 'utf-8'):
        return http_get(self.url, charset)

class HttpResponse:

    def __init__(self, status, headers, content):
        self.status = status
        self.headers = headers
        self.content = content

class HttpError(Exception):
    def __init__(self, status, message):
        self.status = status
        self.message = message
        super().__init__(message)

class HttpFileNotFoundError(HttpError):
    def __init__(self, status, message):
        super().__init__(status, message)

def _do_http(method, url: str, headers, data = None, charset = 'utf-8'):
    """使用低级API访问HTTP，可以任意设置header，data等
    """
    addr = get_host_by_url(url)
    if addr == None:
        raise Exception("invalid url (%s)" % url)

    if url.startswith("https://"):
        conn = http.client.HTTPSConnection(addr)
    else:
        conn = http.client.HTTPConnection(addr)

    headers = headers or dict()
    if "User-Agent" not in headers:
        headers["User-Agent"] = USER_AGENT
    try:
        conn.request(method, url, data, headers = headers)
        buf = None
        response_headers = None
        with conn.getresponse() as resp:
            response_headers = resp.getheaders()
            content_type = resp.getheader("Content-Type")
            content_encoding = resp.getheader("Content-Encoding")
            buf = resp.read()
            return resp.getcode(), response_headers, _decode_resp_bytes(buf, charset, content_encoding)
    finally:
        conn.close()

def _decode_resp_bytes(buf: bytes, charset: str, content_encoding:Optional[str] = None):
    if content_encoding == "gzip":
        fileobj = io.BytesIO(buf)
        gzip_f = gzip.GzipFile(fileobj=fileobj, mode="rb")
        content_bytes = gzip_f.read()
        return codecs.decode(content_bytes, charset)
    elif content_encoding != None:
        raise Exception("暂不支持%s编码" % content_encoding)
    return codecs.decode(buf, charset)

def http_get_by_requests(url, charset = None):
    assert requests != None
    resp = requests.get(url, headers = {"User-Agent": USER_AGENT})
    # 自动检测编码
    resp.encoding = resp.apparent_encoding
    return resp.text

def build_query_string(params, *, skip_empty_value=False):
    temp = []
    for key in params:
        value = params[key]
        if skip_empty_value and (value == None or value == ""):
            continue
        temp.append("%s=%s" % (key, quote(value)))

    return "&".join(temp)

def _join_url_and_params(url, params, *, skip_empty_value=False):
    if params is None:
        return url

    query_string = build_query_string(params, skip_empty_value=skip_empty_value)
    if "?" in url:
        return url + "&" + query_string
    else:
        return url + "?" + query_string

def http_get(url:str, charset=None, params = None, skip_empty_value=False) -> str:
    """Http的GET请求"""
    url = _join_url_and_params(url, params, skip_empty_value=skip_empty_value)

    if _mock != None:
        # 用于单元测试mock
        return _mock.http_get(url, charset, params)

    if requests != None:
        return http_get_by_requests(url, charset)
    out = []
    bufsize = BUFSIZE
    readsize = 0

    # FIXME windows环境下有内存泄漏 安装requests可以解决这个问题
    conn = urlopen(url)
    try:
        chunk = conn.read(bufsize)
        while chunk:
            out.append(chunk)
            readsize += len(chunk)
            chunk = conn.read(bufsize)
        logging.info("get %s bytes", readsize)
        bytes = b''.join(out)
        if charset:
            return codecs.decode(bytes, charset)
        else:
            return try_decode(bytes)
    finally:
        conn.close()

def http_post(url:str, data='', charset='utf-8') -> str:
    """HTTP的POST请求

    :param url: 请求的地址
    :param data: POST请求体
    :param charset: 字符集，默认utf-8

    :raise: 网络异常
    """
    if _mock != None:
        return _mock.http_post(url, data, charset)
    
    if requests != None:
        return _http_post_by_request(url, data, charset)
        
    status, headers, resp_data = _do_http("POST", url, None, data)
    return resp_data

def _http_post_by_request(url: str, data: str, charset='utf-8'):
    assert requests != None
    resp = requests.post(url=url, data=data)
    if not resp.ok:
        raise Exception(f"status_code={resp.status_code}, message={resp.reason}")
    return resp.text

def http_download_by_requests(url, destpath):
    assert requests != None
    resp = requests.get(url, headers = {"User-Agent": USER_AGENT}, stream=True)
    if resp.status_code == 404:
        raise HttpFileNotFoundError(404, f"file {url} not found")
    
    if resp.status_code != 200:
        raise HttpError(resp.status_code, f"StatusCode {resp.status_code}")

    try:
        total_size = int(resp.headers.get('content-length', 0))
    except:
        total_size = -1

    with open(destpath, "wb") as fp:
        readsize = 0
        for chunk in resp.iter_content(chunk_size = BUFSIZE):
            fp.write(chunk)
            readsize += len(chunk)
            logging.info(f"download {readsize} bytes, progress={readsize/total_size*100:.2f}%")
    return resp.headers # headers是key大小写不敏感的dict

def http_download(address: str, destpath: str, dirname = None):
    if dirname is not None:
        basename = os.path.basename(address)
        destpath = os.path.join(dirname, basename)

    assert isinstance(destpath, str)
    destpath = os.path.abspath(destpath)

    if requests is not None:
        return http_download_by_requests(address, destpath)

    bufsize = BUFSIZE
    address = get_http_url(address)
    headers = {
        "User-Agent": USER_AGENT
    }
    request = Request(address, headers = headers)
    stream  = urlopen(request)
    chunk   = stream.read(bufsize)
    dest    = open(destpath, "wb")

    try:
        readsize = 0
        while chunk:
            readsize += len(chunk)
            logging.info("download %s bytes" % readsize)
            dest.write(chunk)
            chunk = stream.read(bufsize)
        logging.info("download %s bytes, saved to %s" % (readsize, destpath))
    finally:
        dest.close()

def tcp_send(domain, port, content, timeout=1, on_recv_func=None):
    """发送TCP请求, 由于TCP协议没有终止标识，超时就返回所有结果
    @param {string} domain 域名
    @param {integer} port 端口号
    @param {bytes|str} content 发送内容
    @param {function} on_recv_func 自定义处理tcp包的函数，返回True继续，返回False中断
    @return {bytes} 返回结果
    """
    TIMEOUT = timeout
    BUFSIZE = 1024

    data = content
    if isinstance(content, str):
        data = content.encode("utf-8")
    ip = socket.gethostbyname(domain)

    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    if hasattr(conn, "settimeout"):
        # timeout单位是秒
        conn.settimeout(TIMEOUT)
    conn.connect((ip, port))
    conn.sendall(data)
    result = []
    try:
        # 使用文件同样会抛出socket.timeout异常，无法判断文件EOF
        # fp = conn.makefile()
        while True:
            buf = conn.recv(BUFSIZE)
            if not buf:break
            result.append(buf)
            if on_recv_func:
                if not on_recv_func(buf):
                    break
    except socket.timeout:
        # FIXME 如果真的是超时怎么办
        return b''.join(result)
    finally:
        conn.close()
    return b''.join(result)


def get_file_ext_by_content_type(content_type):
    if content_type == "image/png":
        return ".png"

    if content_type == "image/jpg":
        return ".jpg"

    if content_type == "image/jpeg":
        return ".jpeg"

    if content_type == "image/gif":
        return ".gif"

    if content_type == "image/webp":
        return ".webp"

    if content_type == "image/svg+xml":
        return ".svg"

    return None


class StructURL:
    def __init__(self):
        self.path = ""
        self.params = {} # type: dict[str, list[str]]

    def get_single_param(self, name=""):
        value = self.params.get(name)
        if isinstance(value, list) and len(value) > 0:
            return value[0]
        return None
    
    def get_list_param(self, name=""):
        value = self.params.get(name)
        if value == None:
            return []
        assert isinstance(value, list)
        return value
    
    def to_url(self):
        if len(self.params) == 0:
            return self.path
        query = ""
        for name in self.params:
            value_list = self.params[name]
            for value in value_list:
                if query != "":
                    query += "&"
                query += f"{name}={value}"
        return self.path + "?" + query

def parse_url(url=""):
    qs_part = url
    path = ""
    if "?" in url:
        parts = url.split("?")
        path = parts[0]
        qs_part = parts[1]

    params_dict = parse_qs(qs_part)
    result = StructURL()
    result.path = path
    result.params = params_dict
    return result

