from __future__ import annotations
from typing import Optional, Union
from typing_extensions import Final

import asyncio
import os
import sys
import colorlog
import re
import argparse
from telethon import TelegramClient
from telethon.tl.types import User, InputPeerUser
from python_socks import parse_proxy_url
from dotenv import load_dotenv
from pathlib import Path
from functools import partial

from .version import __version__


# ----- utils -----
def __bool_parser(var: Optional[str], default_value: bool = False) -> bool:
    if not var:
        return default_value

    if var.isdecimal() or var.lstrip('-').isdecimal():
        return int(var) > 0

    var = var.upper()
    if var in ('FALSE', 'NONE', 'NULL', 'NO', 'NOT', 'DISABLE', 'DISABLED', 'INACTIVE', 'DEACTIVATED', 'OFF'):
        return False
    if var in ('TRUE', 'YES', 'OK', 'ENABLE', 'ENABLED', 'ACTIVE', 'ACTIVATED', 'ON'):
        return True
    return default_value


def __list_parser(var: Optional[str]) -> list[str]:
    return re.split(r'[\s,;，；]+', var.strip()) if var else []


def __decompose_version(version: str) -> list[Union[int, str]]:
    def buf_put(as_int: bool):
        nonlocal buf
        if not buf:
            raise ValueError(f'Empty part in version string: {version}')
        parts.append(int(buf) if as_int else buf)
        buf = ''

    parts = []
    buf = ''
    for i, char in enumerate(version):
        if char == '.':
            buf_put(as_int=True)
            continue
        if not char.isdecimal():
            buf_put(as_int=True)
            parts.append(version[i:])
            break
        buf += char
    if buf:
        buf_put(as_int=True)
    return parts


def __compare_version(version1: str, version2: str) -> tuple[int, list[Union[int, str]], list[Union[int, str]]]:
    """loose comparison, only compare the numeric parts"""
    parts1 = __decompose_version(version1)
    parts2 = __decompose_version(version2)
    marker = 0
    for part1, part2 in zip(parts1, parts2):
        if not (isinstance(part1, int) and isinstance(part2, int)):
            break
        if part1 == part2:
            continue
        marker = 1 if part1 > part2 else -1
        break
    return marker, parts1, parts2


# ----- setup logging -----
__configure_logging = partial(
    colorlog.basicConfig,
    format='%(log_color)s%(asctime)s:%(levelname)s:%(name)s - %(message)s',
    datefmt='%Y-%m-%d-%H:%M:%S'
)
__configure_logging(level=colorlog.DEBUG if __bool_parser(os.environ.get('DEBUG')) else colorlog.INFO)
logger = colorlog.getLogger('RSStT.env')

# ----- determine the environment -----
user_home = os.path.expanduser('~')
self_path = os.path.dirname(__file__)
self_module_name = os.path.basename(self_path)
cli_entry = sys.argv[0]  # expect `-m` or `/path/to/telegramRSSbot.py`
is_self_run_as_a_whole_package = cli_entry.endswith('telegramRSSbot.py')

__arg_parser = argparse.ArgumentParser(
    prog=cli_entry if cli_entry != '-m' else f'python3 -m {self_module_name}',
    description='RSS to Telegram Bot, a Telegram RSS bot that cares about your reading experience.')
__arg_parser.add_argument('-c', '--config', metavar='/path/to/config/folder', type=str, nargs=1,
                          help='path to the config folder')
__arg_parser.add_argument('--dummy', type=str, action='append', nargs=1,
                          help='dummy argument, can be repeated for multiple times. '
                               'Useful to distinguish multiple bot instances on the same host with command line')
cli_args = __arg_parser.parse_args()
custom_config_path = cli_args.config[0] if cli_args.config else None

if custom_config_path:
    config_folder_path = os.path.normpath(os.path.abspath(custom_config_path))
elif is_self_run_as_a_whole_package:
    config_folder_path = os.path.normpath(os.path.join(self_path, '..', 'config'))
else:
    config_folder_path = os.path.join(user_home, '.rsstt')

Path(config_folder_path).mkdir(parents=True, exist_ok=True)
logger.info(f'Config folder: {config_folder_path}')

# ----- load .env -----
dot_env_paths = (os.path.join(config_folder_path, '.env'),
                 os.path.join(os.path.abspath('.'), '.env'))
if is_self_run_as_a_whole_package:
    dot_env_paths = (os.path.normpath(os.path.join(self_path, '..', '.env')),) + dot_env_paths
for dot_env_path in dict.fromkeys(dot_env_paths):  # remove duplicates while keeping the order
    if os.path.isfile(dot_env_path):
        load_dotenv(dot_env_path, override=True)
        logger.info(f'Found .env file at "{dot_env_path}", loaded')


# ----- get version -----
def __get_version():
    version = 'dirty'
    if is_self_run_as_a_whole_package:
        # noinspection PyBroadException
        try:
            with open(os.path.normpath(os.path.join(self_path, '..', '.version')), 'r') as v:
                version = v.read().strip()
        except Exception:
            version = 'dirty'

        if not version or version == '@':
            version = 'dirty'

    if version == 'dirty':
        from subprocess import Popen, PIPE, DEVNULL

        # noinspection PyBroadException
        try:
            with Popen(['git', 'describe', '--tags', '--dirty', '--broken', '--always'],
                       shell=False, stdout=PIPE, stderr=DEVNULL, bufsize=-1) as git:
                git.wait(3)
                version = git.stdout.read().decode().strip()
            with Popen(['git', 'branch', '--show-current'],
                       shell=False, stdout=PIPE, stderr=DEVNULL, bufsize=-1) as git:
                git.wait(3)
                if branch := git.stdout.read().decode().strip():
                    version += f'@{branch}'
        except Exception:
            version = 'dirty'

    try:
        sign, version_decomposed, version_pkg_decomposed = __compare_version(version.lstrip('v'), __version__)
        if sign <= -1:  # outdated version: older than pkg ver
            version = __version__
            if isinstance(version_decomposed[-1], str) and not isinstance(version_pkg_decomposed[-1], str):
                version += re.sub(r'^-\d+(?=-)', '', version_decomposed[-1])  # trim ahead commit count
            version = f'v{version}'
    except ValueError:
        version = f'v{__version__}'

    return version


VERSION: Final = __get_version()

# ----- basic config -----
SAMPLE_APIS: Final = {
    # https://github.com/DrKLO/Telegram/blob/master/TMessagesProj/src/main/java/org/telegram/messenger/BuildVars.java
    4: '014b35b6184100b085b0d0572f9b5103',
    # https://github.com/TelegramMessenger/Telegram-iOS/blob/master/build-system/verify.sh
    8: '7245de8e747a0d6fbe11f7cc14fcc0bb',
    # https://github.com/overtake/TelegramSwift/blob/master/Telegram-Mac/Config.swift
    9: '3975f648bb682ee889f35483bc618d1c',
    # https://github.com/vysheng/tg/blob/master/loop.h
    2899: '36722c72256a24c1225de00eb6a1ca74',
    # https://github.com/telegramdesktop/tdesktop/blob/dev/docs/api_credentials.md
    17349: '344583e45741c457fe1862106095a5eb',
    # https://github.com/tdlib/td/blob/master/example/uwp/app/MainPage.xaml.cs
    94575: 'a3406de8d171bb422bb6ddf3bbd800e2',
    # https://github.com/morethanwords/tweb/blob/master/t.py
    1025907: '452b0359b988148995f22ff0f4229750'
}

API_ID: Final = int(os.environ['API_ID']) if os.environ.get('API_ID') else None
API_HASH: Final = os.environ.get('API_HASH')
TOKEN: Final = os.environ.get('TOKEN')

try:
    _manager = tuple(map(int, __list_parser(os.environ.get('MANAGER') or os.environ.get('CHATID'))))
    MANAGER: Final = set(_manager)  # optimize membership test

    if not all((TOKEN, MANAGER)):
        logger.critical('"TOKEN" OR "MANAGER" NOT SET! PLEASE CHECK YOUR SETTINGS!')
        exit(1)
except Exception as e:
    logger.critical('INVALID "MANAGER"! PLEASE CHECK YOUR SETTINGS!', exc_info=e)
    exit(1)

_error_logging_chat = os.environ.get('ERROR_LOGGING_CHAT')
ERROR_LOGGING_CHAT: Final = int(_error_logging_chat) if _error_logging_chat else _manager[0]
del _error_logging_chat
del _manager

MANAGER_PRIVILEGED: Final = __bool_parser(os.environ.get('MANAGER_PRIVILEGED'))

TELEGRAPH_TOKEN: Final = __list_parser(os.environ.get('TELEGRAPH_TOKEN'))

MULTIUSER: Final = __bool_parser(os.environ.get('MULTIUSER'), default_value=True)

CRON_SECOND: Final = int(os.environ.get('CRON_SECOND') or 0) % 60

# ----- network config -----
DEFAULT_PROXY: Final = os.environ.get('SOCKS_PROXY') or os.environ.get('socks_proxy') \
                       or os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy') \
                       or os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')

TELEGRAM_PROXY: Final = os.environ.get('T_PROXY') or DEFAULT_PROXY
if TELEGRAM_PROXY:
    _parsed = parse_proxy_url(TELEGRAM_PROXY.replace('socks5h', 'socks5'))
    TELEGRAM_PROXY_DICT: Final = {
        'proxy_type': _parsed[0],
        'addr': _parsed[1],
        'port': _parsed[2],
        'username': _parsed[3],
        'password': _parsed[4],
        'rdns': True
    }
    TELEGRAPH_PROXY_DICT: Final = {
        'proxy_type': _parsed[0],
        'host': _parsed[1],
        'port': _parsed[2],
        'username': _parsed[3],
        'password': _parsed[4],
        'rdns': True
    }
    del _parsed
else:
    TELEGRAM_PROXY_DICT: Final = None
    TELEGRAPH_PROXY_DICT: Final = None

R_PROXY: Final = os.environ.get('R_PROXY') or DEFAULT_PROXY

REQUESTS_PROXIES: Final = {'all': R_PROXY} if R_PROXY else {}

PROXY_BYPASS_PRIVATE: Final = __bool_parser(os.environ.get('PROXY_BYPASS_PRIVATE'))
PROXY_BYPASS_DOMAINS: Final = __list_parser(os.environ.get('PROXY_BYPASS_DOMAINS'))
USER_AGENT: Final = os.environ.get('USER_AGENT') or f'RSStT/{__version__} RSS Reader'
IPV6_PRIOR: Final = __bool_parser(os.environ.get('IPV6_PRIOR'))

HTTP_TIMEOUT: Final = int(os.environ.get('HTTP_TIMEOUT') or 12)
HTTP_CONCURRENCY: Final = int(os.environ.get('HTTP_CONCURRENCY') or 1024)
HTTP_CONCURRENCY_PER_HOST: Final = int(os.environ.get('HTTP_CONCURRENCY_PER_HOST') or 16)

# ----- img relay server config -----
_img_relay_server = os.environ.get('IMG_RELAY_SERVER') or 'https://rsstt-img-relay.rongrong.workers.dev/'
IMG_RELAY_SERVER: Final = (
        ('' if _img_relay_server.startswith('http') else 'https://')
        + _img_relay_server
        + ('' if _img_relay_server.endswith(('/', '=')) else '/')
)
del _img_relay_server

# ----- wsrv.nl config -----
_images_weserv_nl = os.environ.get('IMAGES_WESERV_NL') or 'https://wsrv.nl/'
IMAGES_WESERV_NL: Final = (
        ('' if _images_weserv_nl.startswith('http') else 'https://')
        + _images_weserv_nl
        + ('' if _images_weserv_nl.endswith('/') else '/')
)
del _images_weserv_nl

# ----- db config -----
_database_url = os.environ.get('DATABASE_URL') or f'sqlite://{config_folder_path}/db.sqlite3'
DATABASE_URL: Final = (_database_url.replace('postgresql', 'postgres', 1) if _database_url.startswith('postgresql')
                       else _database_url)
del _database_url

# ----- misc config -----
TABLE_TO_IMAGE: Final = __bool_parser(os.environ.get('TABLE_TO_IMAGE'))
TRAFFIC_SAVING: Final = __bool_parser(os.environ.get('TRAFFIC_SAVING'))
LAZY_MEDIA_VALIDATION: Final = __bool_parser(os.environ.get('LAZY_MEDIA_VALIDATION'))
NO_UVLOOP: Final = __bool_parser(os.environ.get('NO_UVLOOP'))
MULTIPROCESSING: Final = __bool_parser(os.environ.get('MULTIPROCESSING'))
EXECUTOR_NICENESS_INCREMENT: Final = int(os.environ.get('EXECUTOR_NICENESS_INCREMENT') or (2 * hasattr(os, 'nice')))
DEBUG: Final = __bool_parser(os.environ.get('DEBUG'))
__configure_logging(  # config twice to make .env file work
    level=colorlog.DEBUG if DEBUG else colorlog.INFO,
    force=True
)
if DEBUG:
    logger.debug('DEBUG mode enabled')

# ----- environment config -----
RAILWAY_STATIC_URL: Final = os.environ.get('RAILWAY_STATIC_URL')
PORT: Final = int(os.environ.get('PORT', 0)) or (8080 if RAILWAY_STATIC_URL else None)

# !!!!! DEPRECATED WARNING !!!!!
if os.environ.get('DELAY'):
    logger.warning('Env var "DELAY" is DEPRECATED and of no use!\n'
                   'To avoid this warning, remove this env var.')

if os.environ.get('CHATID'):
    logger.warning('Env var "CHATID" is DEPRECATED!\n'
                   'To avoid this warning, remove this env var.')

if any((os.environ.get('REDISHOST'), os.environ.get('REDISUSER'), os.environ.get('REDISPASSWORD'),
        os.environ.get('REDISPORT'), os.environ.get('REDIS_NUM'),)):
    logger.warning('Redis DB is DEPRECATED!\n'
                   'ALL SUBS IN THE OLD DB WILL NOT BE MIGRATED. '
                   'IF YOU NEED TO BACKUP YOUR SUBS, DOWNGRADE AND USE "/export" COMMAND TO BACKUP.\n\n'
                   'Please remove these env vars (if exist):\n'
                   'REDISHOST\n'
                   'REDISUSER\n'
                   'REDISPASSWORD\n'
                   'REDISPORT\n'
                   'REDIS_NUM')

if is_self_run_as_a_whole_package and os.path.exists(os.path.join(config_folder_path, 'rss.db')):
    os.rename(os.path.join(config_folder_path, 'rss.db'), os.path.join(config_folder_path, 'rss.db.bak'))
    logger.warning('Sqlite DB "rss.db" with old schema is DEPRECATED and renamed to "rss.db.bak" automatically!\n'
                   'ALL SUBS IN THE OLD DB WILL NOT BE MIGRATED. '
                   'IF YOU NEED TO BACKUP YOUR SUBS, DOWNGRADE AND USE "/export" COMMAND TO BACKUP.')

# ----- shared var -----
bot: Optional[TelegramClient] = None  # placeholder
bot_id: Optional[int] = None  # placeholder
bot_peer: Optional[User] = None  # placeholder
bot_input_peer: Optional[InputPeerUser] = None  # placeholder

# ----- loop initialization -----
uvloop_enabled = False
if not NO_UVLOOP:
    try:
        import uvloop

        uvloop.install()
        uvloop_enabled = True
    except ImportError:  # not installed (e.g. Windows)
        uvloop = None
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
