#!/usr/bin/env python3
# -*-coding:UTF-8 -*

import os
import sys
import re
import uuid
import json
import redis
import datetime
import time
import subprocess
import requests

from collections import deque
from pyfaup.faup import Faup

sys.path.append(os.environ['AIL_BIN'])
from Helper import Process
from pubsublogger import publisher

# ======== FUNCTIONS ========

def load_blacklist(service_type):
    try:
        with open(os.environ['AIL_BIN']+'/torcrawler/blacklist_{}.txt'.format(service_type), 'r') as f:
            redis_crawler.delete('blacklist_{}'.format(service_type))
            lines = f.read().splitlines()
            for line in lines:
                redis_crawler.sadd('blacklist_{}'.format(service_type), line)
    except Exception:
        pass

def update_auto_crawler():
    current_epoch = int(time.time())
    list_to_crawl = redis_crawler.zrangebyscore('crawler_auto_queue', '-inf', current_epoch)
    for elem_to_crawl in list_to_crawl:
        mess, type = elem_to_crawl.rsplit(';', 1)
        redis_crawler.sadd('{}_crawler_priority_queue'.format(type), mess)
        redis_crawler.zrem('crawler_auto_queue', elem_to_crawl)

# Extract info form url (url, domain, domain url, ...)
def unpack_url(url):
    to_crawl = {}
    faup.decode(url)
    url_unpack = faup.get()
    # # FIXME: # TODO: remove me
    try:
        to_crawl['domain'] = url_unpack['domain'].decode()
    except:
        to_crawl['domain'] = url_unpack['domain']
    to_crawl['domain'] = to_crawl['domain'].lower()


    # force lower case domain/subdomain (rfc4343)
    # # FIXME: # TODO: remove me
    try:
        url_host = url_unpack['host'].decode()
    except:
        url_host = url_unpack['host']

    new_url_host = url_host.lower()
    url_lower_case = url.replace(url_host, new_url_host, 1)

    if url_unpack['scheme'] is None:
        to_crawl['scheme'] = 'http'
        url= 'http://{}'.format(url_lower_case)
    else:
        # # FIXME: # TODO: remove me
        try:
            scheme = url_unpack['scheme'].decode()
        except Exception as e:
            scheme = url_unpack['scheme']
        if scheme in default_proto_map:
            to_crawl['scheme'] = scheme
            url = url_lower_case
        else:
            redis_crawler.sadd('new_proto', '{} {}'.format(scheme, url_lower_case))
            to_crawl['scheme'] = 'http'
            url= 'http://{}'.format(url_lower_case.replace(scheme, '', 1))

    if url_unpack['port'] is None:
        to_crawl['port'] = default_proto_map[to_crawl['scheme']]
    else:
        # # FIXME: # TODO: remove me
        try:
            port = url_unpack['port'].decode()
        except:
            port = url_unpack['port']
        # Verify port number                        #################### make function to verify/correct port number
        try:
            int(port)
        # Invalid port Number
        except Exception as e:
            port = default_proto_map[to_crawl['scheme']]
        to_crawl['port'] = port

    #if url_unpack['query_string'] is None:
    #    if to_crawl['port'] == 80:
    #        to_crawl['url']= '{}://{}'.format(to_crawl['scheme'], url_unpack['host'].decode())
    #    else:
    #        to_crawl['url']= '{}://{}:{}'.format(to_crawl['scheme'], url_unpack['host'].decode(), to_crawl['port'])
    #else:
    #    to_crawl['url']= '{}://{}:{}{}'.format(to_crawl['scheme'], url_unpack['host'].decode(), to_crawl['port'], url_unpack['query_string'].decode())

    to_crawl['url'] = url
    if to_crawl['port'] == 80:
        to_crawl['domain_url'] = '{}://{}'.format(to_crawl['scheme'], new_url_host)
    else:
        to_crawl['domain_url'] = '{}://{}:{}'.format(to_crawl['scheme'], new_url_host, to_crawl['port'])

    # # FIXME: # TODO: remove me
    try:
        to_crawl['tld'] = url_unpack['tld'].decode()
    except:
        to_crawl['tld'] = url_unpack['tld']

    return to_crawl

# get url, paste and service_type to crawl
def get_elem_to_crawl(rotation_mode):
    message = None
    domain_service_type = None

    #load_priority_queue
    for service_type in rotation_mode:
        message = redis_crawler.spop('{}_crawler_priority_queue'.format(service_type))
        if message is not None:
            domain_service_type = service_type
            break
    #load_normal_queue
    if message is None:
        for service_type in rotation_mode:
            message = redis_crawler.spop('{}_crawler_queue'.format(service_type))
            if message is not None:
                domain_service_type = service_type
                break

    if message:
        splitted = message.rsplit(';', 1)
        if len(splitted) == 2:
            url, paste = splitted
            if paste:
                paste = paste.replace(PASTES_FOLDER+'/', '')

        message = {'url': url, 'paste': paste, 'type_service': domain_service_type, 'original_message': message}

    return message

def get_crawler_config(redis_server, mode, service_type, domain, url=None):
    crawler_options = {}
    if mode=='auto':
        config = redis_server.get('crawler_config:{}:{}:{}:{}'.format(mode, service_type, domain, url))
    else:
        config = redis_server.get('crawler_config:{}:{}:{}'.format(mode, service_type, domain))
    if config is None:
        config = {}
    else:
        config = json.loads(config)
    for option in default_crawler_config:
        if option in config:
            crawler_options[option] = config[option]
        else:
            crawler_options[option] = default_crawler_config[option]
    if mode == 'auto':
        crawler_options['time'] = int(config['time'])
    elif mode == 'manual':
        redis_server.delete('crawler_config:{}:{}:{}'.format(mode, service_type, domain))
    return crawler_options

def load_crawler_config(service_type, domain, paste, url, date):
    crawler_config = {}
    crawler_config['splash_url'] = splash_url
    crawler_config['item'] = paste
    crawler_config['service_type'] = service_type
    crawler_config['domain'] = domain
    crawler_config['date'] = date

    # Auto and Manual Crawling
    # Auto ################################################# create new entry, next crawling => here or when ended ?
    if paste == 'auto':
        crawler_config['crawler_options'] = get_crawler_config(redis_crawler, 'auto', service_type, domain, url=url)
        crawler_config['requested'] = True
    # Manual
    elif paste == 'manual':
        crawler_config['crawler_options'] = get_crawler_config(r_cache, 'manual', service_type, domain)
        crawler_config['requested'] = True
    # default crawler
    else:
        crawler_config['crawler_options'] = get_crawler_config(redis_crawler, 'default', service_type, domain)
        crawler_config['requested'] = False
    return crawler_config

def is_domain_up_day(domain, type_service, date_day):
    if redis_crawler.sismember('{}_up:{}'.format(type_service, date_day), domain):
        return True
    else:
        return False

def set_crawled_domain_metadata(type_service, date, domain, father_item):
    # first seen
    if not redis_crawler.hexists('{}_metadata:{}'.format(type_service, domain), 'first_seen'):
        redis_crawler.hset('{}_metadata:{}'.format(type_service, domain), 'first_seen', date['date_day'])

    redis_crawler.hset('{}_metadata:{}'.format(type_service, domain), 'paste_parent', father_item)
    # last check
    redis_crawler.hset('{}_metadata:{}'.format(type_service, domain), 'last_check', date['date_day'])

# Put message back on queue
def on_error_send_message_back_in_queue(type_service, domain, message):
    if not redis_crawler.sismember('{}_domain_crawler_queue'.format(type_service), domain):
        redis_crawler.sadd('{}_domain_crawler_queue'.format(type_service), domain)
        redis_crawler.sadd('{}_crawler_priority_queue'.format(type_service), message)

def crawl_onion(url, domain, port, type_service, message, crawler_config):
    crawler_config['url'] = url
    crawler_config['port'] = port
    print('Launching Crawler: {}'.format(url))

    r_cache.hset('metadata_crawler:{}'.format(splash_port), 'crawling_domain', domain)
    r_cache.hset('metadata_crawler:{}'.format(splash_port), 'started_time', datetime.datetime.now().strftime("%Y/%m/%d  -  %H:%M.%S"))

    retry = True
    nb_retry = 0
    while retry:
        try:
            r = requests.get(splash_url , timeout=30.0)
            retry = False
        except Exception:
            # TODO: relaunch docker or send error message
            nb_retry += 1

            if nb_retry == 6:
                on_error_send_message_back_in_queue(type_service, domain, message)
                publisher.error('{} SPASH DOWN'.format(splash_url))
                print('--------------------------------------')
                print('         \033[91m DOCKER SPLASH DOWN\033[0m')
                print('          {} DOWN'.format(splash_url))
                r_cache.hset('metadata_crawler:{}'.format(splash_port), 'status', 'SPLASH DOWN')
                nb_retry == 0

            print('         \033[91m DOCKER SPLASH NOT AVAILABLE\033[0m')
            print('          Retry({}) in 10 seconds'.format(nb_retry))
            time.sleep(10)

    if r.status_code == 200:
        r_cache.hset('metadata_crawler:{}'.format(splash_port), 'status', 'Crawling')
        # save config in cash
        UUID = str(uuid.uuid4())
        r_cache.set('crawler_request:{}'.format(UUID), json.dumps(crawler_config))

        process = subprocess.Popen(["python", './torcrawler/tor_crawler.py', UUID],
                                   stdout=subprocess.PIPE)
        while process.poll() is None:
            time.sleep(1)

        if process.returncode == 0:
            output = process.stdout.read().decode()
            print(output)
            # error: splash:Connection to proxy refused
            if 'Connection to proxy refused' in output:
                on_error_send_message_back_in_queue(type_service, domain, message)
                publisher.error('{} SPASH, PROXY DOWN OR BAD CONFIGURATION'.format(splash_url))
                print('------------------------------------------------------------------------')
                print('         \033[91m SPLASH: Connection to proxy refused')
                print('')
                print('            PROXY DOWN OR BAD CONFIGURATION\033[0m'.format(splash_url))
                print('------------------------------------------------------------------------')
                r_cache.hset('metadata_crawler:{}'.format(splash_port), 'status', 'Error')
                exit(-2)
        else:
            print(process.stdout.read())
            exit(-1)
    else:
        on_error_send_message_back_in_queue(type_service, domain, message)
        print('--------------------------------------')
        print('         \033[91m DOCKER SPLASH DOWN\033[0m')
        print('          {} DOWN'.format(splash_url))
        r_cache.hset('metadata_crawler:{}'.format(splash_port), 'status', 'Crawling')
        exit(1)

# check external links (full_crawl)
def search_potential_source_domain(type_service, domain):
    external_domains = set()
    for link in redis_crawler.smembers('domain_{}_external_links:{}'.format(type_service, domain)):
        # unpack url
        url_data = unpack_url(link)
        if url_data['domain'] != domain:
            if url_data['tld'] == 'onion' or url_data['tld'] == 'i2p':
                external_domains.add(url_data['domain'])
    # # TODO: add special tag ?
    if len(external_domains) >= 20:
        redis_crawler.sadd('{}_potential_source'.format(type_service), domain)
        print('New potential source found: domain')
    redis_crawler.delete('domain_{}_external_links:{}'.format(type_service, domain))


if __name__ == '__main__':

    if len(sys.argv) != 2:
        print('usage:', 'Crawler.py', 'splash_port')
        exit(1)
##################################################
    #mode = sys.argv[1]
    splash_port = sys.argv[1]

    rotation_mode = deque(['onion', 'regular'])
    default_proto_map = {'http': 80, 'https': 443}
######################################################## add ftp ???

    publisher.port = 6380
    publisher.channel = "Script"
    publisher.info("Script Crawler started")
    config_section = 'Crawler'

    # Setup the I/O queues
    p = Process(config_section)

    splash_url = '{}:{}'.format( p.config.get("Crawler", "splash_url_onion"),  splash_port)
    print('splash url: {}'.format(splash_url))

    PASTES_FOLDER = os.path.join(os.environ['AIL_HOME'], p.config.get("Directories", "pastes"))

    r_serv_metadata = redis.StrictRedis(
        host=p.config.get("ARDB_Metadata", "host"),
        port=p.config.getint("ARDB_Metadata", "port"),
        db=p.config.getint("ARDB_Metadata", "db"),
        decode_responses=True)

    r_cache = redis.StrictRedis(
        host=p.config.get("Redis_Cache", "host"),
        port=p.config.getint("Redis_Cache", "port"),
        db=p.config.getint("Redis_Cache", "db"),
        decode_responses=True)

    redis_crawler = redis.StrictRedis(
        host=p.config.get("ARDB_Onion", "host"),
        port=p.config.getint("ARDB_Onion", "port"),
        db=p.config.getint("ARDB_Onion", "db"),
        decode_responses=True)

    faup = Faup()

    # Default crawler options
    default_crawler_config = {'html': 1,
                              'har': 1,
                              'png': 1,
                              'depth_limit': p.config.getint("Crawler", "crawler_depth_limit"),
                              'closespider_pagecount': 50,
                              'user_agent': 'Mozilla/5.0 (Windows NT 6.1; rv:24.0) Gecko/20100101 Firefox/24.0'}

    # Track launched crawler
    r_cache.sadd('all_crawler', splash_port)
    r_cache.hset('metadata_crawler:{}'.format(splash_port), 'status', 'Waiting')
    r_cache.hset('metadata_crawler:{}'.format(splash_port), 'started_time', datetime.datetime.now().strftime("%Y/%m/%d  -  %H:%M.%S"))

    # update hardcoded blacklist
    load_blacklist('onion')
    load_blacklist('regular')

    while True:

        update_auto_crawler()

        rotation_mode.rotate()
        to_crawl = get_elem_to_crawl(rotation_mode)
        if to_crawl:
            url_data = unpack_url(to_crawl['url'])
            # remove domain from queue
            redis_crawler.srem('{}_domain_crawler_queue'.format(to_crawl['type_service']), url_data['domain'])

            print()
            print()
            print('\033[92m------------------START CRAWLER------------------\033[0m')
            print('crawler type:     {}'.format(to_crawl['type_service']))
            print('\033[92m-------------------------------------------------\033[0m')
            print('url:         {}'.format(url_data['url']))
            print('domain:      {}'.format(url_data['domain']))
            print('domain_url:  {}'.format(url_data['domain_url']))
            print()

            # Check blacklist
            if not redis_crawler.sismember('blacklist_{}'.format(to_crawl['type_service']), url_data['domain']):
                date = {'date_day': datetime.datetime.now().strftime("%Y%m%d"),
                        'date_month': datetime.datetime.now().strftime("%Y%m"),
                        'epoch': int(time.time())}

                # Update crawler status type
                r_cache.sadd('{}_crawlers'.format(to_crawl['type_service']), splash_port)

                crawler_config = load_crawler_config(to_crawl['type_service'], url_data['domain'], to_crawl['paste'],  to_crawl['url'], date)
                # check if default crawler
                if not crawler_config['requested']:
                    # Auto crawl only if service not up this month
                    if redis_crawler.sismember('month_{}_up:{}'.format(to_crawl['type_service'], date['date_month']), url_data['domain']):
                        continue

                set_crawled_domain_metadata(to_crawl['type_service'], date, url_data['domain'], to_crawl['paste'])


                #### CRAWLER ####
                # Manual and Auto Crawler
                if crawler_config['requested']:

                ######################################################crawler strategy
                    # CRAWL domain
                    crawl_onion(url_data['url'], url_data['domain'], url_data['port'], to_crawl['type_service'], to_crawl['original_message'], crawler_config)

                # Default Crawler
                else:
                    # CRAWL domain
                    crawl_onion(url_data['domain_url'], url_data['domain'], url_data['port'], to_crawl['type_service'], to_crawl['original_message'], crawler_config)
                    #if url != domain_url and not is_domain_up_day(url_data['domain'], to_crawl['type_service'], date['date_day']):
                    #    crawl_onion(url_data['url'], url_data['domain'], to_crawl['original_message'])


                # Save last_status day (DOWN)
                if not is_domain_up_day(url_data['domain'], to_crawl['type_service'], date['date_day']):
                    redis_crawler.sadd('{}_down:{}'.format(to_crawl['type_service'], date['date_day']), url_data['domain'])

                # if domain was UP at least one time
                if redis_crawler.exists('crawler_history_{}:{}:{}'.format(to_crawl['type_service'], url_data['domain'], url_data['port'])):
                    # add crawler history (if domain is down)
                    if not redis_crawler.zrangebyscore('crawler_history_{}:{}:{}'.format(to_crawl['type_service'], url_data['domain'], url_data['port']), date['epoch'], date['epoch']):
                        # Domain is down
                        redis_crawler.zadd('crawler_history_{}:{}:{}'.format(to_crawl['type_service'], url_data['domain'], url_data['port']), int(date['epoch']), int(date['epoch']))

                        ############################
                        # extract page content
                        ############################

                # update list, last crawled domains
                redis_crawler.lpush('last_{}'.format(to_crawl['type_service']), '{}:{};{}'.format(url_data['domain'], url_data['port'], date['epoch']))
                redis_crawler.ltrim('last_{}'.format(to_crawl['type_service']), 0, 15)

                #update crawler status
                r_cache.hset('metadata_crawler:{}'.format(splash_port), 'status', 'Waiting')
                r_cache.hdel('metadata_crawler:{}'.format(splash_port), 'crawling_domain')

                # Update crawler status type
                r_cache.srem('{}_crawlers'.format(to_crawl['type_service']), splash_port)

                # add next auto Crawling in queue:
                if to_crawl['paste'] == 'auto':
                    redis_crawler.zadd('crawler_auto_queue', int(time.time()+crawler_config['crawler_options']['time']) , '{};{}'.format(to_crawl['original_message'], to_crawl['type_service']))
                    # update list, last auto crawled domains
                    redis_crawler.lpush('last_auto_crawled', '{}:{};{}'.format(url_data['domain'], url_data['port'], date['epoch']))
                    redis_crawler.ltrim('last_auto_crawled', 0, 9)
            else:
                print('                 Blacklisted Domain')
                print()
                print()

        else:
            time.sleep(1)
