#  Polkascan PRE Harvester
#
#  Copyright 2018-2020 openAware BV (NL).
#  This file is part of Polkascan.
#
#  Polkascan is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Polkascan 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with Polkascan. If not, see <http://www.gnu.org/licenses/>.
#
#  harvester.py

import datetime
import uuid

import falcon
import pytz
from celery.result import AsyncResult
from falcon.media.validators.jsonschema import validate
from sqlalchemy import text, func

from app import settings
from app.models.data import Block, BlockTotal
from app.models.harvester import Setting, Status
from app.resources.base import BaseResource
from app.schemas import load_schema
from app.processors.converters import PolkascanHarvesterService, BlockAlreadyAdded, BlockIntegrityError
from substrateinterface import SubstrateInterface
from app.tasks import accumulate_block_recursive, start_harvester, rebuild_search_index, rebuild_account_info_snapshot
from app.settings import SUBSTRATE_RPC_URL, TYPE_REGISTRY, TYPE_REGISTRY_FILE


class PolkascanStartHarvesterResource(BaseResource):

    #@validate(load_schema('start_harvester'))
    def on_post(self, req, resp):

        task = start_harvester.delay(check_gaps=True)

        resp.status = falcon.HTTP_201

        resp.media = {
            'status': 'success',
            'data': {
                'task_id': task.id
            }
        }


class PolkascanStopHarvesterResource(BaseResource):

    def on_post(self, req, resp):

        resp.status = falcon.HTTP_404

        resp.media = {
            'status': 'success',
            'data': {
                'message': 'TODO'
            }
        }


class PolkaScanCheckHarvesterTaskResource(BaseResource):

    def on_get(self, req, resp, task_id):

        task_result = AsyncResult(task_id)
        result = {'status': task_result.status, 'result': task_result.result}
        resp.status = falcon.HTTP_200
        resp.media = result


class PolkascanHarvesterQueueResource(BaseResource):

    def on_get(self, req, resp):

        last_known_block = Block.query(self.session).order_by(Block.id.desc()).first()

        if not last_known_block:
            resp.media = {
                'status': 'success',
                'data': {
                    'message': 'Harvester waiting for first run'
                }
            }
        else:

            remaining_sets_result = Block.get_missing_block_ids(self.session)

            resp.status = falcon.HTTP_200

            resp.media = {
                'status': 'success',
                'data': {
                    'harvester_head': last_known_block.id,
                    'block_process_queue': [
                        {'from': block_set['block_from'], 'to': block_set['block_to']}
                        for block_set in remaining_sets_result
                    ]
                }
            }


class PolkascanHarvesterStatusResource(BaseResource):

    def on_get(self, req, resp):

        sequencer_task = Status.get_status(self.session, 'SEQUENCER_TASK_ID')
        integrity_head = Status.get_status(self.session, 'INTEGRITY_HEAD')
        sequencer_head = self.session.query(func.max(BlockTotal.id)).one()[0]
        best_block = Block.query(self.session).filter_by(
            id=self.session.query(func.max(Block.id)).one()[0]).first()

        if best_block:
            best_block_datetime = best_block.datetime.replace(tzinfo=pytz.UTC).timestamp() * 1000
            best_block_nr = best_block.id
        else:
            best_block_datetime = None
            best_block_nr = None

        substrate = SubstrateInterface(SUBSTRATE_RPC_URL)
        chain_head_block_id = substrate.get_block_number(substrate.get_chain_head())
        chain_finalized_block_id = substrate.get_block_number(substrate.get_chain_finalised_head())

        resp.media = {
            'best_block_datetime': best_block_datetime,
            'best_block_nr': best_block_nr,
            'sequencer_task': sequencer_task.value,
            'sequencer_head': sequencer_head,
            'integrity_head': int(integrity_head.value),
            'chain_head_block_id': chain_head_block_id,
            'chain_finalized_block_id': chain_finalized_block_id
        }


class PolkascanProcessBlockResource(BaseResource):

    def on_post(self, req, resp):

        block_hash = None

        if req.media.get('block_id'):
            substrate = SubstrateInterface(SUBSTRATE_RPC_URL)
            block_hash = substrate.get_block_hash(req.media.get('block_id'))
        elif req.media.get('block_hash'):
            block_hash = req.media.get('block_hash')
        else:
            resp.status = falcon.HTTP_BAD_REQUEST
            resp.media = {'errors': ['Either block_hash or block_id should be supplied']}

        if block_hash:
            print('Processing {} ...'.format(block_hash))
            harvester = PolkascanHarvesterService(
                db_session=self.session,
                type_registry=TYPE_REGISTRY,
                type_registry_file=TYPE_REGISTRY_FILE
            )

            block = Block.query(self.session).filter(Block.hash == block_hash).first()

            if block:
                resp.status = falcon.HTTP_200
                resp.media = {'result': 'already exists', 'parentHash': block.parent_hash}
            else:

                amount = req.media.get('amount', 1)

                for nr in range(0, amount):
                    try:
                        block = harvester.add_block(block_hash)
                    except BlockAlreadyAdded as e:
                        print('Skipping {}'.format(block_hash))
                    block_hash = block.parent_hash
                    if block.id == 0:
                        break

                self.session.commit()

                resp.status = falcon.HTTP_201
                resp.media = {'result': 'added', 'parentHash': block.parent_hash}

        else:
            resp.status = falcon.HTTP_404
            resp.media = {'result': 'Block not found'}


class SequenceBlockResource(BaseResource):

    def on_post(self, req, resp):

        block_hash = None

        if 'block_id' in req.media:
            block = Block.query(self.session).filter(Block.id == req.media.get('block_id')).first()
        elif req.media.get('block_hash'):
            block_hash = req.media.get('block_hash')
            block = Block.query(self.session).filter(Block.hash == block_hash).first()
        else:
            block = None
            resp.status = falcon.HTTP_BAD_REQUEST
            resp.media = {'errors': ['Either block_hash or block_id should be supplied']}

        if block:
            print('Sequencing #{} ...'.format(block.id))

            harvester = PolkascanHarvesterService(
                db_session=self.session,
                type_registry=TYPE_REGISTRY,
                type_registry_file=TYPE_REGISTRY_FILE
            )

            if block.id == 1:
                # Add genesis block
                parent_block = harvester.add_block(block.parent_hash)

            block_total = BlockTotal.query(self.session).filter_by(id=block.id).first()
            parent_block = Block.query(self.session).filter(Block.id == block.id - 1).first()
            parent_block_total = BlockTotal.query(self.session).filter_by(id=block.id - 1).first()

            if block_total:
                resp.status = falcon.HTTP_200
                resp.media = {'result': 'already exists', 'blockId': block.id}
            else:

                if parent_block_total:
                    parent_block_total = parent_block_total.asdict()

                if parent_block:
                    parent_block = parent_block.asdict()

                harvester.sequence_block(block, parent_block, parent_block_total)

                self.session.commit()

                resp.status = falcon.HTTP_201
                resp.media = {'result': 'added', 'parentHash': block.parent_hash}

        else:
            resp.status = falcon.HTTP_404
            resp.media = {'result': 'Block not found'}


class StartSequenceBlockResource(BaseResource):

    def on_post(self, req, resp):

        sequencer_task = Status.get_status(self.session, 'SEQUENCER_TASK_ID')

        if sequencer_task.value is None:
            # 3. IF NOT RUNNING: set task id is status table
            sequencer_task.value = "123"
            sequencer_task.save(self.session)

            harvester = PolkascanHarvesterService(
                db_session=self.session,
                type_registry=TYPE_REGISTRY,
                type_registry_file=TYPE_REGISTRY_FILE
            )
            result = harvester.start_sequencer()

            sequencer_task.value = None
            sequencer_task.save(self.session)

        # 4. IF RUNNING: check if task id is active
        else:

            # task_result = AsyncResult(sequencer_task)
            # task_result = {'status': task_result.status, 'result': task_result.result}
            sequencer_task.value = None
            sequencer_task.save(self.session)

            result = 'Busy'

        self.session.commit()

        resp.media = {
            'result': result
        }


class ProcessGenesisBlockResource(BaseResource):

    def on_post(self, req, resp):

        harvester = PolkascanHarvesterService(
            db_session=self.session,
            type_registry=TYPE_REGISTRY,
            type_registry_file=TYPE_REGISTRY_FILE
        )
        block = Block.query(self.session).get(1)
        if block:
            result = harvester.process_genesis(block=block)
        else:
            result = 'Block #1 required to process genesis'

        self.session.commit()

        resp.media = {
            'result': result
        }


class StartIntegrityResource(BaseResource):

    def on_post(self, req, resp):
        harvester = PolkascanHarvesterService(
            db_session=self.session,
            type_registry=TYPE_REGISTRY,
            type_registry_file=TYPE_REGISTRY_FILE
        )
        try:
            result = harvester.integrity_checks()
        except BlockIntegrityError as e:
            result = str(e)

        resp.media = {
            'result': result
        }


class RebuildSearchIndexResource(BaseResource):

    def on_post(self, req, resp):
        if settings.CELERY_RUNNING:
            task = rebuild_search_index.delay()
            data = {
                'task_id': task.id
            }
        else:
            data = rebuild_search_index()

        resp.status = falcon.HTTP_201

        resp.media = {
            'status': 'Search index rebuild task created',
            'data': data
        }


class RebuildAccountInfoResource(BaseResource):

    def on_post(self, req, resp):
        if settings.CELERY_RUNNING:
            task = rebuild_account_info_snapshot.delay()
            data = {
                'task_id': task.id
            }
        else:
            data = rebuild_account_info_snapshot()

        resp.status = falcon.HTTP_201

        resp.media = {
            'status': 'Search index rebuild task created',
            'data': data
        }