# -*- coding: utf-8 -*-
"""Define Dashboard related utilities and miscellaneous logic.

Copyright (C) 2020 Gitcoin Core

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
import base64
import json
import logging
import re
from json.decoder import JSONDecodeError

from django.conf import settings
from django.urls import URLPattern, URLResolver
from django.utils import timezone

import ipfshttpclient
import requests
from app.utils import sync_profile
from avatar.models import CustomAvatar
from compliance.models import Country, Entity
from cytoolz import compose
from dashboard.helpers import UnsupportedSchemaException, normalize_url, process_bounty_changes, process_bounty_details
from dashboard.models import (
    Activity, BlockedUser, Bounty, BountyFulfillment, HackathonRegistration, Profile, UserAction,
)
from dashboard.sync.btc import sync_btc_payout
from dashboard.sync.celo import sync_celo_payout
from dashboard.sync.etc import sync_etc_payout
from dashboard.sync.eth import sync_eth_payout
from dashboard.sync.polkadot import sync_polkadot_payout
from dashboard.sync.zil import sync_zil_payout
from eth_abi import decode_single, encode_single
from eth_utils import keccak, to_checksum_address, to_hex
from gas.utils import conf_time_spread, eth_usd_conv_rate, gas_advisories, recommend_min_gas_price_to_confirm_in_time
from hexbytes import HexBytes
from ipfshttpclient.exceptions import CommunicationError
from pytz import UTC
from web3 import HTTPProvider, Web3, WebsocketProvider
from web3.exceptions import BadFunctionCallOutput
from web3.middleware import geth_poa_middleware

from .abi import erc20_abi
from .notifications import maybe_market_to_slack

logger = logging.getLogger(__name__)

SEMAPHORE_BOUNTY_SALT = '1'
SEMAPHORE_BOUNTY_NS = 'bounty_processor'


def all_sendcryptoasset_models():
    from revenue.models import DigitalGoodPurchase
    from dashboard.models import Tip
    from kudos.models import KudosTransfer

    return [DigitalGoodPurchase, Tip, KudosTransfer]


class ProfileNotFoundException(Exception):
    pass


class ProfileHiddenException(Exception):
    pass


class BountyNotFoundException(Exception):
    pass


class UnsupportedNetworkException(Exception):
    pass


class IPFSCantConnectException(Exception):
    pass


class NoBountiesException(Exception):
    pass


def humanize_event_name(name):
    """Humanize an event name.

    Args:
      name (str): The event name

    Returns:
        str: The humanized representation.

    """
    humanized_event_names = {
        'new_bounty': 'New funded issue',
        'start_work': 'Work started',
        'stop_work': 'Work stopped',
        'work_submitted': 'Work submitted',
        'increased_bounty': 'Increased funds for issue',
        'killed_bounty': 'Cancelled funded issue',
        'worker_approved': 'Worker approved',
        'worker_rejected': 'Worker rejected',
        'work_done': 'Work done',
        'issue_remarketed': 'Issue re-marketed'
    }

    return humanized_event_names.get(name, name).upper()


def create_user_action(user, action_type, request=None, metadata=None):
    """Create a UserAction for the specified action type.

    Args:
        user (User): The User object.
        action_type (str): The type of action to record.
        request (Request): The request object. Defaults to: None.
        metadata (dict): Any accompanying metadata to be added.
            Defaults to: {}.

    Returns:
        bool: Whether or not the UserAction was created successfully.

    """
    from app.utils import handle_location_request
    if action_type not in dict(UserAction.ACTION_TYPES).keys():
        print('UserAction.create_action received an invalid action_type')
        return False

    if metadata is None:
        metadata = {}

    kwargs = {
        'metadata': metadata,
        'action': action_type,
        'user': user
    }

    if request:
        geolocation_data, ip_address = handle_location_request(request)

        if geolocation_data:
            kwargs['location_data'] = geolocation_data
        if ip_address:
            kwargs['ip_address'] = ip_address

        utmJson = _get_utm_from_cookie(request)

        if utmJson:
            kwargs['utm'] = utmJson

    if user and hasattr(user, 'profile'):
        kwargs['profile'] = user.profile if user and user.profile else None

    try:
        UserAction.objects.create(**kwargs)
        return True
    except Exception as e:
        logger.error(f'Failure in UserAction.create_action - ({e})')
        return False


def _get_utm_from_cookie(request):
    """Extract utm* params from Cookie.

    Args:
        request (Request): The request object.

    Returns:
        utm_source: if it's not in cookie should be None.
        utm_medium: if it's not in cookie should be None.
        utm_campaign: if it's not in cookie should be None.

    """
    utmDict = {}
    utm_source = request.COOKIES.get('utm_source')
    if not utm_source:
        utm_source = request.GET.get('utm_source')
    utm_medium = request.COOKIES.get('utm_medium')
    if not utm_medium:
        utm_medium = request.GET.get('utm_medium')
    utm_campaign = request.COOKIES.get('utm_campaign')
    if not utm_campaign:
        utm_campaign = request.GET.get('utm_campaign')

    if utm_source:
        utmDict['utm_source'] = utm_source
    if utm_medium:
        utmDict['utm_medium'] = utm_medium
    if utm_campaign:
        utmDict['utm_campaign'] = utm_campaign

    if bool(utmDict):
        return utmDict
    else:
        return None


def get_ipfs(host=None, port=settings.IPFS_API_PORT):
    """Establish a connection to IPFS.

    Args:
        host (str): The IPFS host to connect to.
            Defaults to environment variable: IPFS_HOST.  The host name should be of the form 'ipfs.infura.io' and not
            include 'https://'.
        port (int): The IPFS port to connect to.
            Defaults to environment variable: env IPFS_API_PORT.

    Raises:
        CommunicationError: The exception is raised when there is a
            communication error with IPFS.

    Returns:
        ipfshttpclient.client.Client: The IPFS connection client.

    """
    if host is None:
        clientConnectString = f'/dns/{settings.IPFS_HOST}/tcp/{settings.IPFS_API_PORT}/{settings.IPFS_API_SCHEME}'
    else:
        clientConnectString = f'/dns/{host}/tcp/{settings.IPFS_API_PORT}/https'
    try:
        return ipfshttpclient.connect(clientConnectString)
    except CommunicationError as e:
        logger.exception(e)
        raise IPFSCantConnectException('Failed while attempt to connect to IPFS')
    return None


def ipfs_cat(key):
    try:
        # Attempt connecting to IPFS via Infura
        response, status_code = ipfs_cat_requests(key)
        if status_code == 200:
            return response

        # Attempt connecting to IPFS via hosted node
        response = ipfs_cat_ipfsapi(key)
        if response:
            return response

        raise IPFSCantConnectException('Failed to connect cat key against IPFS - Check IPFS/Infura connectivity')
    except IPFSCantConnectException as e:
        logger.exception(e)


def ipfs_cat_ipfsapi(key):
    ipfs = get_ipfs()
    if ipfs:
        try:
            return ipfs.cat(key)
        except Exception:
            return None


def ipfs_cat_requests(key):
    try:
        url = f'https://ipfs.infura.io:5001/api/v0/cat?arg={key}'
        response = requests.get(url, timeout=1)
        return response.text, response.status_code
    except:
        return None, 500


def get_web3(network, sockets=False):
    """Get a Web3 session for the provided network.

    Attributes:
        network (str): The network to establish a session with.

    Raises:
        UnsupportedNetworkException: The exception is raised if the method
            is passed an invalid network.

    Returns:
        web3.main.Web3: A web3 instance for the provided network.

    """
    if network in ['mainnet', 'rinkeby', 'ropsten']:
        if sockets:
            if settings.INFURA_USE_V3:
                provider = WebsocketProvider(f'wss://{network}.infura.io/ws/v3/{settings.INFURA_V3_PROJECT_ID}')
            else:
                provider = WebsocketProvider(f'wss://{network}.infura.io/ws')
        else:
            if settings.INFURA_USE_V3:
                provider = HTTPProvider(f'https://{network}.infura.io/v3/{settings.INFURA_V3_PROJECT_ID}')
            else:
                provider = HTTPProvider(f'https://{network}.infura.io')

        w3 = Web3(provider)
        if network == 'rinkeby':
            w3.middleware_stack.inject(geth_poa_middleware, layer=0)
        return w3
    elif network == 'localhost' or 'custom network':
        return Web3(Web3.HTTPProvider("http://testrpc:8545", request_kwargs={'timeout': 60}))

    raise UnsupportedNetworkException(network)


def get_profile_from_referral_code(code):
    """Returns a profile from the unique code

    Returns:
        A unique string for each profile
    """
    return base64.urlsafe_b64decode(code.encode()).decode()


def get_bounty_invite_url(inviter, bounty_id):
    """Returns a unique url for each bounty and one who is inviting
    Returns:
        A unique string for each bounty
    """
    salt = "X96gRAVvwx52uS6w4QYCUHRfR3OaoB"
    string = str(inviter) + salt + str(bounty_id)
    return base64.urlsafe_b64encode(string.encode()).decode()


def get_bounty_from_invite_url(invite_url):
    """Returns a unique url for each bounty and one who is inviting

    Returns:
        A unique string for each bounty
    """
    salt = "X96gRAVvwx52uS6w4QYCUHRfR3OaoB"
    decoded_string = base64.urlsafe_b64decode(invite_url.encode()).decode()
    data_array = decoded_string.split(salt)
    inviter = data_array[0]
    bounty = data_array[1]
    return {'inviter': inviter, 'bounty': bounty}


def get_unrated_bounties_count(user):
    if not user:
        return 0
    unrated_contributed = Bounty.objects.current().prefetch_related('feedbacks').filter(interested__profile=user) \
        .filter(interested__status='okay') \
        .filter(interested__pending=False).filter(idx_status='submitted') \
        .exclude(
            feedbacks__feedbackType='worker',
            feedbacks__sender_profile=user,
        )
    unrated_funded = Bounty.objects.current().prefetch_related('fulfillments', 'interested', 'interested__profile', 'feedbacks') \
    .filter(
        bounty_owner_github_username__iexact=user.handle,
        idx_status='done'
    ).exclude(
        feedbacks__feedbackType='approver',
        feedbacks__sender_profile=user,
    )
    unrated_count = unrated_funded.count() + unrated_contributed.count()
    return unrated_count


def getStandardBountiesContractAddresss(network):
    if network == 'mainnet':
        return to_checksum_address('0x2af47a65da8cd66729b4209c22017d6a5c2d2400')
    elif network == 'rinkeby':
        return to_checksum_address('0xf209d2b723b6417cbf04c07e733bee776105a073')
    raise UnsupportedNetworkException(network)


# http://web3py.readthedocs.io/en/latest/contracts.html
def getBountyContract(network):
    web3 = get_web3(network)
    standardbounties_abi = '[{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"}],"name":"killBounty","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_bountyId","type":"uint256"}],"name":"getBountyToken","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_data","type":"string"}],"name":"fulfillBounty","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newDeadline","type":"uint256"}],"name":"extendDeadline","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getNumBounties","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_fulfillmentId","type":"uint256"},{"name":"_data","type":"string"}],"name":"updateFulfillment","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newFulfillmentAmount","type":"uint256"},{"name":"_value","type":"uint256"}],"name":"increasePayout","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newFulfillmentAmount","type":"uint256"}],"name":"changeBountyFulfillmentAmount","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newIssuer","type":"address"}],"name":"transferIssuer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_value","type":"uint256"}],"name":"activateBounty","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_issuer","type":"address"},{"name":"_deadline","type":"uint256"},{"name":"_data","type":"string"},{"name":"_fulfillmentAmount","type":"uint256"},{"name":"_arbiter","type":"address"},{"name":"_paysTokens","type":"bool"},{"name":"_tokenContract","type":"address"}],"name":"issueBounty","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_issuer","type":"address"},{"name":"_deadline","type":"uint256"},{"name":"_data","type":"string"},{"name":"_fulfillmentAmount","type":"uint256"},{"name":"_arbiter","type":"address"},{"name":"_paysTokens","type":"bool"},{"name":"_tokenContract","type":"address"},{"name":"_value","type":"uint256"}],"name":"issueAndActivateBounty","outputs":[{"name":"","type":"uint256"}],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"_bountyId","type":"uint256"}],"name":"getBountyArbiter","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_value","type":"uint256"}],"name":"contribute","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newPaysTokens","type":"bool"},{"name":"_newTokenContract","type":"address"}],"name":"changeBountyPaysTokens","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_bountyId","type":"uint256"}],"name":"getBountyData","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_fulfillmentId","type":"uint256"}],"name":"getFulfillment","outputs":[{"name":"","type":"bool"},{"name":"","type":"address"},{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newArbiter","type":"address"}],"name":"changeBountyArbiter","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newDeadline","type":"uint256"}],"name":"changeBountyDeadline","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_fulfillmentId","type":"uint256"}],"name":"acceptFulfillment","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"bounties","outputs":[{"name":"issuer","type":"address"},{"name":"deadline","type":"uint256"},{"name":"data","type":"string"},{"name":"fulfillmentAmount","type":"uint256"},{"name":"arbiter","type":"address"},{"name":"paysTokens","type":"bool"},{"name":"bountyStage","type":"uint8"},{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_bountyId","type":"uint256"}],"name":"getBounty","outputs":[{"name":"","type":"address"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"bool"},{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_bountyId","type":"uint256"},{"name":"_newData","type":"string"}],"name":"changeBountyData","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_bountyId","type":"uint256"}],"name":"getNumFulfillments","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_owner","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"}],"name":"BountyIssued","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"},{"indexed":false,"name":"issuer","type":"address"}],"name":"BountyActivated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"},{"indexed":true,"name":"fulfiller","type":"address"},{"indexed":true,"name":"_fulfillmentId","type":"uint256"}],"name":"BountyFulfilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_bountyId","type":"uint256"},{"indexed":false,"name":"_fulfillmentId","type":"uint256"}],"name":"FulfillmentUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"},{"indexed":true,"name":"fulfiller","type":"address"},{"indexed":true,"name":"_fulfillmentId","type":"uint256"}],"name":"FulfillmentAccepted","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"},{"indexed":true,"name":"issuer","type":"address"}],"name":"BountyKilled","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"},{"indexed":true,"name":"contributor","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"ContributionAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"},{"indexed":false,"name":"newDeadline","type":"uint256"}],"name":"DeadlineExtended","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"bountyId","type":"uint256"}],"name":"BountyChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_bountyId","type":"uint256"},{"indexed":true,"name":"_newIssuer","type":"address"}],"name":"IssuerTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_bountyId","type":"uint256"},{"indexed":false,"name":"_newFulfillmentAmount","type":"uint256"}],"name":"PayoutIncreased","type":"event"}]'
    standardbounties_addr = getStandardBountiesContractAddresss(network)
    bounty_abi = json.loads(standardbounties_abi)
    getBountyContract = web3.eth.contract(standardbounties_addr, abi=bounty_abi)
    return getBountyContract


def get_bounty(bounty_enum, network):
    if (settings.DEBUG or settings.ENV != 'prod') and network == 'mainnet':
        # This block will return {} if env isn't prod and the network is mainnet.
        print("--*--")
        return {}

    standard_bounties = getBountyContract(network)

    try:
        issuer, contract_deadline, fulfillmentAmount, paysTokens, bountyStage, balance = standard_bounties.functions.getBounty(bounty_enum).call()
    except BadFunctionCallOutput:
        raise BountyNotFoundException
    # pull from blockchain
    bountydata = standard_bounties.functions.getBountyData(bounty_enum).call()
    arbiter = standard_bounties.functions.getBountyArbiter(bounty_enum).call()
    token = standard_bounties.functions.getBountyToken(bounty_enum).call()
    bounty_data_str = ipfs_cat(bountydata)
    bounty_data = json.loads(bounty_data_str)

    # fulfillments
    num_fulfillments = int(standard_bounties.functions.getNumFulfillments(bounty_enum).call())
    fulfillments = []
    for fulfill_enum in range(0, num_fulfillments):

        # pull from blockchain
        accepted, fulfiller, data = standard_bounties.functions.getFulfillment(bounty_enum, fulfill_enum).call()
        try:
            data_str = ipfs_cat(data)
            data = json.loads(data_str)
        except JSONDecodeError:
            logger.error(f'Could not get {data} from ipfs')
            continue

        # validation
        if 'Failed to get block' in str(data_str):
            raise IPFSCantConnectException("Failed to connect to IPFS")

        fulfillments.append({
            'id': fulfill_enum,
            'accepted': accepted,
            'fulfiller': fulfiller,
            'data': data,
        })

    # validation
    if 'Failed to get block' in str(bounty_data_str):
        raise IPFSCantConnectException("Failed to connect to IPFS")

    # https://github.com/Bounties-Network/StandardBounties/issues/25
    ipfs_deadline = bounty_data.get('payload', {}).get('expire_date', False)
    deadline = contract_deadline
    if ipfs_deadline:
        deadline = ipfs_deadline

    # assemble the data
    bounty = {
        'id': bounty_enum,
        'issuer': issuer,
        'deadline': deadline,
        'contract_deadline': contract_deadline,
        'ipfs_deadline': ipfs_deadline,
        'fulfillmentAmount': fulfillmentAmount,
        'paysTokens': paysTokens,
        'bountyStage': bountyStage,
        'balance': balance,
        'data': bounty_data,
        'arbiter': arbiter,
        'token': token,
        'fulfillments': fulfillments,
        'network': network,
        'review': bounty_data.get('review',{}),
    }
    return bounty


# processes a bounty returned by get_bounty
def web3_process_bounty(bounty_data):
    """Process web3 bounty data by creating new or updated Bounty objects."""
    # Check whether or not the bounty data payload is for mainnet and env is prod or other network and not mainnet.
    if not bounty_data or (settings.DEBUG or settings.ENV != 'prod') and bounty_data.get('network') == 'mainnet':
        # This block will return None if running in debug/non-prod env and the network is mainnet.
        print(f"--*--")
        return None

    did_change, old_bounty, new_bounty = process_bounty_details(bounty_data)

    if did_change and new_bounty:
        _from = old_bounty.pk if old_bounty else None
        print(f"- processing changes, {_from} => {new_bounty.pk}")
        process_bounty_changes(old_bounty, new_bounty)

    return did_change, old_bounty, new_bounty


def has_tx_mined(txid, network):
    web3 = get_web3(network)
    try:
        transaction = web3.eth.getTransaction(txid)
        if not transaction:
            return False
        if not transaction.blockHash:
            return False
        return transaction.blockHash != HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000')
    except Exception:
        return False


def sync_payout(fulfillment):
    token_name = fulfillment.token_name
    if not token_name:
        token_name = fulfillment.bounty.token_name

    if fulfillment.payout_type == 'web3_modal':
        sync_eth_payout(fulfillment)

    elif fulfillment.payout_type == 'qr':
        if token_name == 'ETC':
            sync_etc_payout(fulfillment)
        elif token_name == 'BTC':
            sync_btc_payout(fulfillment)
        elif token_name == 'CELO' or token_name == 'cUSD':
            sync_celo_payout(fulfillment)
        elif token_name == 'ZIL':
            sync_zil_payout(fulfillment)

    elif fulfillment.payout_type == 'polkadot_ext':
         sync_polkadot_payout(fulfillment)


def get_bounty_id(issue_url, network):
    issue_url = normalize_url(issue_url)
    bounty_id = get_bounty_id_from_db(issue_url, network)
    if bounty_id:
        return bounty_id

    all_known_stdbounties = Bounty.objects.filter(
        web3_type='bounties_network',
        network=network,
    ).nocache().order_by('-standard_bounties_id')

    try:
        highest_known_bounty_id = get_highest_known_bounty_id(network)
        bounty_id = get_bounty_id_from_web3(issue_url, network, highest_known_bounty_id, direction='down')
    except NoBountiesException:
        last_known_bounty_id = 0
        if all_known_stdbounties.exists():
            last_known_bounty_id = all_known_stdbounties.first().standard_bounties_id
        bounty_id = get_bounty_id_from_web3(issue_url, network, last_known_bounty_id, direction='up')

    return bounty_id


def get_bounty_id_from_db(issue_url, network):
    issue_url = normalize_url(issue_url)
    bounties = Bounty.objects.filter(
        github_url=issue_url,
        network=network,
        web3_type='bounties_network',
    ).nocache().order_by('-standard_bounties_id')
    if not bounties.exists():
        return None
    return bounties.first().standard_bounties_id


def get_highest_known_bounty_id(network):
    standard_bounties = getBountyContract(network)
    num_bounties = int(standard_bounties.functions.getNumBounties().call())
    if num_bounties == 0:
        raise NoBountiesException()
    return num_bounties - 1


def get_bounty_id_from_web3(issue_url, network, start_bounty_id, direction='up'):
    issue_url = normalize_url(issue_url)

    # iterate through all the bounties
    bounty_enum = start_bounty_id
    more_bounties = True
    while more_bounties:
        try:

            # pull and process each bounty
            print(f'** get_bounty_id_from_web3; looking at {bounty_enum}')
            bounty = get_bounty(bounty_enum, network)
            url = bounty.get('data', {}).get('payload', {}).get('webReferenceURL', False)
            if url == issue_url:
                return bounty['id']

        except BountyNotFoundException:
            more_bounties = False
        except UnsupportedSchemaException:
            pass
        finally:
            # prepare for next loop
            if direction == 'up':
                bounty_enum += 1
            else:
                bounty_enum -= 1

    return None


def build_profile_pairs(bounty):
    """Build the profile pairs list of tuples for ingestion by notifications.

    Args:
        bounty (dashboard.models.Bounty): The Bounty to build profile pairs for.

    Returns:
        list of tuples: The list of profile pair tuples.

    """
    profile_handles = []
    for fulfillment in bounty.fulfillments.select_related('profile').all().order_by('pk'):
        if fulfillment.profile and fulfillment.profile.handle.strip() and fulfillment.profile.absolute_url:
            profile_handles.append((fulfillment.profile.handle, fulfillment.profile.absolute_url))
        else:
            if bounty.tenant == 'ETH':
                addr = f"https://etherscan.io/address/{fulfillment.fulfiller_address}"
            elif bounty.tenant == 'ZIL':
                addr = f"https://viewblock.io/zilliqa/address/{fulfillment.fulfiller_address}"
            elif bounty.tenant == 'CELO':
                addr = f"https://explorer.celo.org/address/{fulfillment.fulfiller_address}"
            elif bounty.tenant == 'ETC':
                addr = f"https://blockscout.com/etc/mainnet/address/{fulfillment.fulfiller_address}"
            elif bounty.tenant == 'BTC':
                addr = f"https://blockstream.info/api/address/{fulfillment.fulfiller_address}"
            else:
                addr = None
            profile_handles.append((fulfillment.fulfiller_address, addr))
    return profile_handles


def get_ordinal_repr(num):
    """Handle cardinal to ordinal representation of numeric values.

    Args:
        num (int): The integer to be converted from cardinal to ordinal numerals.

    Returns:
        str: The ordinal representation of the provided integer.

    """
    ordinal_suffixes = {1: 'st', 2: 'nd', 3: 'rd'}
    if 10 <= num % 100 <= 20:
        suffix = 'th'
    else:
        suffix = ordinal_suffixes.get(num % 10, 'th')
    return f'{num}{suffix}'


def record_funder_inaction_on_fulfillment(bounty_fulfillment):
    payload = {
        'profile': bounty_fulfillment.bounty.bounty_owner_profile,
        'metadata': {
            'bounties': list(bounty_fulfillment.bounty.pk),
            'bounty_fulfillment_pk': bounty_fulfillment.pk,
            'needs_review': True
        }
    }
    Activity.objects.create(activity_type='bounty_abandonment_escalation_to_mods', bounty=bounty_fulfillment.bounty, **payload)


def record_user_action_on_interest(interest, event_name, last_heard_from_user_days):
    """Record User actions and activity for the associated Interest."""
    payload = {
        'profile': interest.profile,
        'metadata': {
            'bounties': list(interest.bounty_set.values_list('pk', flat=True)),
            'interest_pk': interest.pk,
            'last_heard_from_user_days': last_heard_from_user_days,
        }
    }
    UserAction.objects.create(action=event_name, **payload)

    if event_name in ['bounty_abandonment_escalation_to_mods', 'bounty_abandonment_warning']:
        payload['needs_review'] = True

    Activity.objects.create(activity_type=event_name, bounty=interest.bounty_set.last(), **payload)


def get_context(ref_object=None, github_username='', user=None, confirm_time_minutes_target=4,
                confirm_time_slow=120, confirm_time_avg=15, confirm_time_fast=1, active='',
                title='', update=None):
    """Get the context dictionary for use in view."""
    context = {
        'githubUsername': github_username,  # TODO: Deprecate this field.
        'action_urls': ref_object.action_urls() if hasattr(ref_object, 'action_urls') else None,
        'active': active,
        'recommend_gas_price': recommend_min_gas_price_to_confirm_in_time(confirm_time_minutes_target),
        'recommend_gas_price_slow': recommend_min_gas_price_to_confirm_in_time(confirm_time_slow),
        'recommend_gas_price_avg': recommend_min_gas_price_to_confirm_in_time(confirm_time_avg),
        'recommend_gas_price_fast': recommend_min_gas_price_to_confirm_in_time(confirm_time_fast),
        'eth_usd_conv_rate': eth_usd_conv_rate(),
        'conf_time_spread': conf_time_spread(),
        'email': getattr(user, 'email', ''),
        'handle': getattr(user, 'username', ''),
        'title': title,
        'gas_advisories': gas_advisories(),
    }
    if ref_object is not None:
        context.update({f'{ref_object.__class__.__name__}'.lower(): ref_object})
    if update is not None and isinstance(update, dict):
        context.update(update)
    return context


def clean_bounty_url(url):
    """Clean the Bounty URL of unsavory characters.

    The primary utility of this method is to drop #issuecomment blocks from
    Github issue URLs copy/pasted via comments.

    Args:
        url (str): The Bounty VC URL.

    TODO:
        * Deprecate this in favor of Django forms.

    Returns:
        str: The cleaned Bounty URL.

    """
    try:
        return url.split('#')[0]
    except Exception:
        return url


def generate_pub_priv_keypair():
    # Thanks https://github.com/vkobel/ethereum-generate-wallet/blob/master/LICENSE.md
    from ecdsa import SigningKey, SECP256k1
    import sha3

    def checksum_encode(addr_str):
        keccak = sha3.keccak_256()
        out = ''
        addr = addr_str.lower().replace('0x', '')
        keccak.update(addr.encode('ascii'))
        hash_addr = keccak.hexdigest()
        for i, c in enumerate(addr):
            if int(hash_addr[i], 16) >= 8:
                out += c.upper()
            else:
                out += c
        return '0x' + out

    keccak = sha3.keccak_256()

    priv = SigningKey.generate(curve=SECP256k1)
    pub = priv.get_verifying_key().to_string()

    keccak.update(pub)
    address = keccak.hexdigest()[24:]

    def test(addrstr):
        assert(addrstr == checksum_encode(addrstr))

    test('0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed')
    test('0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359')
    test('0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB')
    test('0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb')
    test('0x7aA3a964CC5B0a76550F549FC30923e5c14EDA84')

    # print("Private key:", priv.to_string().hex())
    # print("Public key: ", pub.hex())
    # print("Address:    ", checksum_encode(address))
    # return priv key, pub key, address

    return priv.to_string().hex(), pub.hex(), checksum_encode(address)


def get_bounty_semaphore_ns(standard_bounty_id):
    return f'{SEMAPHORE_BOUNTY_NS}_{standard_bounty_id}_{SEMAPHORE_BOUNTY_SALT}'


def release_bounty_lock(standard_bounty_id):
    from app.utils import release_semaphore
    ns = get_bounty_semaphore_ns(standard_bounty_id)
    release_semaphore(ns)


def profile_helper(handle, suppress_profile_hidden_exception=False, current_user=None, disable_cache=False, full_profile=False):
    """Define the profile helper.

    Args:
        handle (str): The profile handle.

    Raises:
        DoesNotExist: The exception is raised if a Profile isn't found matching the handle.
            Remediation is attempted by syncing the profile data.
        MultipleObjectsReturned: The exception is raised if multiple Profiles are found.
            The latest Profile will be returned.

    Returns:
        dashboard.models.Profile: The Profile associated with the provided handle.

    """

    current_profile = getattr(current_user, 'profile', None)
    if current_profile and current_profile.handle == handle:
        return current_profile

    base = Profile.objects if not full_profile else Profile.objects_full
    try:
        if disable_cache:
            base = base.nocache()
        profile = base.get(handle=handle.lower())
    except Profile.DoesNotExist:
        profile = sync_profile(handle)
        if not profile:
            raise ProfileNotFoundException
    except Profile.MultipleObjectsReturned as e:
        # Handle edge case where multiple Profile objects exist for the same handle.
        # We should consider setting Profile.handle to unique.
        # TODO: Should we handle merging or removing duplicate profiles?
        profile = base.filter(handle=handle.lower()).latest('id')
        logging.error(e)

    if profile.hide_profile and not profile.is_org and not suppress_profile_hidden_exception:
        raise ProfileHiddenException

    return profile

def is_valid_eth_address(eth_address):
    return (bool(re.match(r"^0x[a-zA-Z0-9]{40}$", eth_address)) or eth_address == "0x0")

def get_tx_status(txid, network, created_on):
    from django.utils import timezone
    from dashboard.utils import get_web3
    import pytz

    DROPPED_DAYS = 4

    # get status
    status = None
    if txid == 'override':
        return 'success', None #overridden by admin
    try:
        web3 = get_web3(network)
        tx = web3.eth.getTransactionReceipt(txid)
        if not tx:
            drop_dead_date = created_on + timezone.timedelta(days=DROPPED_DAYS)
            if timezone.now() > drop_dead_date:
                status = 'dropped'
            else:
                status = 'pending'
        elif tx and 'status' not in tx.keys():
            if bool(tx['blockNumber']) and bool(tx['blockHash']):
                status = 'success'
            else:
                raise Exception("got a tx but no blockNumber or blockHash")
        elif tx.status == 1:
            status = 'success'
        elif tx.status == 0:
            status = 'error'
        else:
            status = 'unknown'
    except Exception as e:
        logger.debug(f'Failure in get_tx_status for {txid} - ({e})')
        status = 'unknown'

    # get timestamp
    timestamp = None
    try:
        if tx:
            block = web3.eth.getBlock(tx['blockNumber'])
            timestamp = block.timestamp
            timestamp = timezone.datetime.fromtimestamp(timestamp).replace(tzinfo=pytz.UTC)
    except:
        pass
    return status, timestamp


def is_blocked(handle):
    # check admin block list
    is_on_blocked_list = BlockedUser.objects.filter(handle__icontains=handle, active=True).exists()
    if is_on_blocked_list:
        return True

    # check banned country list
    profiles = Profile.objects.filter(handle=handle.lower())
    if profiles.exists():
        profile = profiles.first()
        last_login = profile.actions.filter(action='Login').order_by('pk').last()
        if last_login:
            last_login_country = last_login.location_data.get('country_name')
            if last_login_country:
                is_on_banned_countries = Country.objects.filter(name=last_login_country)
                if is_on_banned_countries:
                    return True

        # check banned entity list
        if profile.user:
            first_name = profile.user.first_name
            last_name = profile.user.last_name
            full_name = '{first_name} {last_name}'
            is_on_banned_user_list = Entity.objects.filter(fullName__icontains=full_name)
            if is_on_banned_user_list:
                return True

    return False


def get_nonce(network, address, ignore_db=False):
    # this function solves the problem of 2 pending tx's writing over each other
    # by checking both web3 RPC *and* the local DB for the nonce
    # and then using the higher of the two as the tx nonce
    from perftools.models import JSONStore
    from dashboard.utils import get_web3
    w3 = get_web3(network)

    # web3 RPC node: nonce
    nonce_from_web3 = w3.eth.getTransactionCount(address)
    if ignore_db:
        return nonce_from_web3

    # db storage
    key = f"nonce_{network}_{address}"
    view = 'get_nonce'
    nonce_from_db = 0
    try:
        nonce_from_db = JSONStore.objects.get(key=key, view=view).data[0]
        nonce_from_db += 1 # increment by 1 bc we need to be 1 higher than last txid
    except:
        pass

    new_nonce = max(nonce_from_db, nonce_from_web3)

    # update JSONStore
    JSONStore.objects.filter(key=key, view=view).all().delete()
    JSONStore.objects.create(key=key, view=view, data=[new_nonce])

    return new_nonce


def re_market_bounty(bounty, auto_save = True):
    remarketed_count = bounty.remarketed_count
    if remarketed_count < settings.RE_MARKET_LIMIT:
        now = timezone.now()
        minimum_wait_after_remarketing = bounty.last_remarketed + timezone.timedelta(minutes=settings.MINUTES_BETWEEN_RE_MARKETING) if bounty.last_remarketed else now
        if now >= minimum_wait_after_remarketing:
            bounty.remarketed_count = remarketed_count + 1
            bounty.last_remarketed = now

            if auto_save:
                bounty.save()

            maybe_market_to_slack(bounty, 'issue_remarketed')

            result_msg = 'The issue will appear at the top of the issue explorer. '
            further_permitted_remarket_count = settings.RE_MARKET_LIMIT - bounty.remarketed_count
            if further_permitted_remarket_count >= 1:
                result_msg += f'You will be able to remarket this bounty {further_permitted_remarket_count} more time if a contributor does not pick this up.'
            elif further_permitted_remarket_count == 0:
                result_msg += 'Please note this is the last time the issue is able to be remarketed.'

            return {
                "success": True,
                "msg": result_msg
            }
        else:
            time_delta_wait_time = minimum_wait_after_remarketing - now
            minutes_to_wait = round(time_delta_wait_time.total_seconds() / 60)
            return {
                "success": False,
                "msg": f'As you recently remarketed this issue, you need to wait {minutes_to_wait} minutes before remarketing this issue again.'
            }
    else:
        return {
            "success": False,
            "msg": f'The issue was not remarketed due to reaching the remarket limit ({settings.RE_MARKET_LIMIT}).'
        }


def apply_new_bounty_deadline(bounty, deadline, auto_save = True):
    bounty.expires_date = timezone.make_aware(
        timezone.datetime.fromtimestamp(deadline),
        timezone=UTC)
    result = re_market_bounty(bounty, False)

    if auto_save:
        bounty.save()

    base_result_msg = "You've extended expiration of this issue."
    result['msg'] = base_result_msg + " " + result['msg']

    return result


def release_bounty_to_the_public(bounty, auto_save = True):
    if bounty:
        bounty.reserved_for_user_handle = None
        bounty.reserved_for_user_from = None
        bounty.reserved_for_user_expiration = None

        if auto_save:
            bounty.save()

        return True
    else:
        return False


def get_orgs_perms(profile):
    orgs = profile.profile_organizations.all()

    response_data = []
    for org in orgs:
        org_perms = {'name': org.name, 'users': []}
        groups = org.groups.all().filter(user__isnull=False)
        for g in groups: # "admin", "write", "pull", "none"
            group_data = g.name.split('-')
            if group_data[1] != "role": #skip repo level groups
                continue
            org_perms['users'] = [{
                'handle': u.profile.handle,
                'role': group_data[2],
                'name': '{} {}'.format(u.first_name, u.last_name)
            } for u in g.user_set.prefetch_related('profile').all()]
        response_data.append(org_perms)
    return response_data

def get_all_urls():
    urlconf = __import__(settings.ROOT_URLCONF, {}, {}, [''])

    def list_urls(lis, acc=None):
        if acc is None:
            acc = []
        if not lis:
            return
        l = lis[0]
        if isinstance(l, URLPattern):
            yield acc + [str(l.pattern)]
        elif isinstance(l, URLResolver):
            yield from list_urls(l.url_patterns, acc + [str(l.pattern)])

        yield from list_urls(lis[1:], acc)

    return list_urls(urlconf.urlpatterns)

def get_url_first_indexes():

    return ['_administration','about','action','actions','activity','api','avatar','blog','bounties','bounty','btctalk','casestudies','casestudy','chat','community','contributor','contributor_dashboard','credit','dashboard','docs','dynamic','explorer','extension','faucet','fb','feedback','funder','funder_dashboard','funding','gas','ghlogin','github','gitter','grant','grants','hackathon','hackathonlist','hackathons','health','help','home','how','impersonate','inbox','interest','issue','itunes','jobs','jsi18n','kudos','l','labs','landing','lazy_load_kudos','lbcheck','leaderboard','legacy','legal','livestream','login','logout','mailing_list','medium','mission','modal','new','not_a_token','o','onboard','podcast','postcomment','press','presskit','products','profile','quests','reddit','refer','register_hackathon','requestincrease','requestmoney','requests','results','revenue','robotstxt','schwag','send','service','settings','sg_sendgrid_event_processor','sitemapsectionxml','sitemapxml','slack','spec','strbounty_network','submittoken','sync','terms','tip','townsquare','tribe','tribes','twitter','users','verified','vision','wallpaper','wallpapers','web3','whitepaper','wiki','wikiazAZ09azdAZdazd','youtube']
    # TODO: figure out the recursion issue with the URLs at a later date
    # or just cache them in the backend dynamically

    urls = []
    for p in get_all_urls():
        url = p[0].split('/')[0]
        url = re.sub(r'\W+', '', url)
        urls.append(url)

    return set(urls)

def get_custom_avatars(profile):
    return CustomAvatar.objects.filter(profile=profile).order_by('-id')


def _trim_null_address(address):
    if address == '0x0000000000000000000000000000000000000000':
        return '0x0'
    else:
        return address


def _construct_transfer_filter_params(
        recipient_address,
        token_address,):

    event_topic = keccak(text="Transfer(address,address,uint256)")

    # [event, from, to]
    topics = [
        to_hex(event_topic),
        None,
        to_hex(encode_single('address', recipient_address))
    ]

    return {
        "topics": topics,
        "fromBlock": 0,
        "toBlock": "latest",
        "address": token_address,
    }


def _extract_sender_address_from_log(log):
    return decode_single("address", log['topics'][1])


def get_token_recipient_senders(network, recipient_address, token_address):
    w3 = get_web3(network)

    contract = w3.eth.contract(
        address=token_address,
        abi=erc20_abi,
    )

    # TODO: This can be made less brittle/opaque
    # see usage of contract.events.Transfer.getLogs in
    # commit 99a44cd3036ace8fcd886ed1e96747528f105d10
    # after migrating to web3 >= 5.
    filter_params = _construct_transfer_filter_params(
        recipient_address,
        token_address,
    )

    logs = w3.eth.getLogs(filter_params)

    process_log = compose(
        _trim_null_address,
        _extract_sender_address_from_log
    )

    return [process_log(log) for log in logs]


def get_hackathons_page_default_tabs():
    from perftools.models import JSONStore

    return JSONStore.objects.get(key='hackathons', view='hackathons').data[0]


def get_hackathon_events():
    from perftools.models import JSONStore

    return JSONStore.objects.get(key='hackathons', view='hackathons').data[1]


def set_hackathon_event(type, event):
    return {
        'type': type,
        'name': event.name,
        'slug': event.slug,
        'background_color': event.background_color,
        'logo': event.logo.name or event.logo_svg.name,
        'start_date': event.start_date.isoformat(),
        'end_date': event.end_date.isoformat(),
        'summary': event.hackathon_summary,
        'sponsor_profiles': [
            {
                'absolute_url': sponsor.absolute_url,
                'avatar_url': sponsor.avatar_url,
            }
            for sponsor in event.sponsor_profiles.all()
        ],
        'display_showcase': event.display_showcase,
        'show_results': event.show_results,
    }
