import datetime
from math import ceil
from typing import List, Optional, cast

from flask import Response, request, session, url_for
from flask_restx import Resource, fields

from decksite import APP, auth, league
from decksite.data import archetype as archs
from decksite.data import card as cs
from decksite.data import competition as comp
from decksite.data import deck, match
from decksite.data import person as ps
from decksite.data import query
from decksite.data import rule as rs
from decksite.data.achievements import Achievement
from decksite.prepare import prepare_decks
from decksite.views import DeckEmbed
from magic import oracle, rotation, tournaments
from magic.decklist import parse_line
from magic.models import Deck
from shared import configuration, dtutil, guarantee, redis
from shared.pd_exception import DoesNotExistException, InvalidDataException, TooManyItemsException
from shared_web import template
from shared_web.api import generate_error, return_json, validate_api_key
from shared_web.decorators import fill_args, fill_form

#pylint: disable=no-self-use

DECK_ENTRY = APP.api.model('DecklistEntry', {
    'n': fields.Integer(),
    'name': fields.String()
})

DECK = APP.api.model('Deck', {
    'id': fields.Integer(readonly=True),
    'name': fields.String(),
    'created_date': fields.DateTime(),
    'updated_date': fields.DateTime(),
    'wins': fields.Integer(),
    'losses': fields.Integer(),
    'finish': fields.Integer(),
    'archetype_id': fields.Integer(),
    'archetype_name': fields.String(),
    'source_url': fields.String(),
    'competition_id': fields.Integer(),
    'competition_name': fields.String(),
    'person': fields.String(),
    'decklist_hash': fields.String(),
    'retired': fields.Boolean(),
    'colors': fields.List(fields.String()),
    'omw': fields.Integer(),
    'season_id': fields.Integer(),
    'maindeck': fields.List(fields.Nested(DECK_ENTRY)),
    'sideboard': fields.List(fields.Nested(DECK_ENTRY)),
})

COMPETITION = APP.api.model('Competition', {
    'id': fields.Integer(readonly=True),
    'name': fields.String(),
    'start_date': fields.DateTime(),
    'end_date': fields.DateTime(),
    # 'url': fields.Url('competition'),
    'top_n': fields.Integer(),
    'num_decks': fields.Integer(),
    'num_reviewed': fields.Integer(),
    'sponsor_name': fields.String(),
    'series_name': fields.String(),
    'type': fields.String(),
    'season_id': fields.Integer(),
    'decks': fields.List(fields.Nested(DECK))
})

@APP.route('/api/decks/')
def decks_api() -> Response:
    """
    Grab a slice of results from a 0-indexed resultset of decks.
    Input:
        {
            'archetypeId': <int?>,
            'cardName': <str?>,
            'competitionId': <int?>,
            'personId': <int?>,
            'deckType': <'league'|'tournament'|'all'>,
            'page': <int>,
            'pageSize': <int>,
            'personId': <int?>,
            'sortBy': <str>,
            'sortOrder': <'ASC'|'DESC'>,
            'seasonId': <int|'all'>
        }
    Output:
        {
            'page': <int>,
            'pages': <int>,
            'decks': [<deck>]
        }
    """
    if not request.args.get('sortBy') and request.args.get('competitionId'):
        sort_by = 'top8'
        sort_order = 'ASC'
    elif not request.args.get('sortBy'):
        sort_by = 'date'
        sort_order = 'DESC'
    else:
        sort_by = str(request.args.get('sortBy'))
        sort_order = str(request.args.get('sortOrder'))
    assert sort_order in ['ASC', 'DESC']
    order_by = query.decks_order_by(sort_by, sort_order)
    page_size = int(request.args.get('pageSize', 20))
    page = int(request.args.get('page', 0))
    start = page * page_size
    limit = f'LIMIT {start}, {page_size}'
    # Don't restrict by season if we're loading something with a date by its id.
    season_id = 'all' if request.args.get('competitionId') else rotation.season_id(str(request.args.get('seasonId')), None)
    where = query.decks_where(request.args, session.get('person_id'))
    total = deck.load_decks_count(where=where, season_id=season_id)
    pages = max(ceil(total / page_size) - 1, 0) # 0-indexed
    ds = deck.load_decks(where=where, order_by=order_by, limit=limit, season_id=season_id)
    prepare_decks(ds)
    r = {'page': page, 'pages': pages, 'decks': ds}
    resp = return_json(r, camelize=True)
    resp.set_cookie('page_size', str(page_size))
    return resp

@APP.api.route('/decks/<int:deck_id>')
class LoadDeck(Resource):
    @APP.api.marshal_with(DECK)
    def get(self, deck_id: int) -> Deck:
        return deck.load_deck(deck_id)

@APP.api.route('/randomlegaldeck')
class LoadRandomDeck(Resource):
    @APP.api.marshal_with(DECK)
    def get(self) -> Optional[Deck]:
        blob = league.random_legal_deck()
        if blob is None:
            APP.api.abort(404, 'No legal decks could be found')
            return None
        blob['url'] = url_for('deck', deck_id=blob['id'], _external=True)
        return blob

@APP.route('/api/competitions')
def competitions_api() -> Response:
    # Don't send competitions with any decks that do not have their correct archetype to third parties otherwise they
    # will store it and be wrong forever.
    comps = comp.load_competitions(having='num_reviewed = num_decks', should_load_decks=True)
    r = []
    for c in comps:
        if c.decks:
            cr = {}
            cr['id'] = c.id
            cr['name'] = c.name
            cr['url'] = url_for('competition_api', competition_id=c.id, _external=True)
            r.append(cr)
    return return_json(r) # type: ignore

@APP.route('/api/competitions/<competition_id>')
def competition_api(competition_id: int) -> Response:
    return return_json(comp.load_competition(competition_id))

@APP.api.route('/league')
class League(Resource):
    @APP.api.marshal_with(COMPETITION)
    def get(self) -> comp.Competition:
        lg = league.active_league(should_load_decks=True)
        pdbot = request.form.get('api_token', None) == configuration.get('pdbot_api_token')
        if not pdbot:
            lg.decks = [d for d in lg.decks if not d.is_in_current_run()]
        return lg

@APP.route('/api/person/<person>')
@fill_args('season_id')
def person_api(person: str, season_id: int = -1) -> Response:
    if season_id == -1:
        season_id = rotation.current_season_num()
    try:
        p = ps.load_person_by_discord_id_or_username(person, season_id)
        p.decks_url = url_for('person_decks_api', person=person, season_id=season_id)
        p.head_to_head = url_for('person_h2h_api', person=person, season_id=season_id)
        return return_json(p)
    except DoesNotExistException:
        return return_json(generate_error('NOTFOUND', 'Person does not exist'))

@APP.route('/api/person/<person>/decks')
@fill_args('season_id')
def person_decks_api(person: str, season_id: int = 0) -> Response:
    p = ps.load_person_by_discord_id_or_username(person, season_id=season_id)
    blob = {
        'name': p.name,
        'decks': p.decks,
    }
    return return_json(blob)

@APP.route('/api/person/<person>/h2h')
@fill_args('season_id')
def person_h2h_api(person: str, season_id: int = 0) -> Response:
    p = ps.load_person_by_discord_id_or_username(person, season_id=season_id)
    return return_json(p.head_to_head)

@APP.route('/api/league/run/<person>')
def league_run_api(person: str) -> Response:
    decks = league.active_decks_by(person)
    if len(decks) == 0:
        return return_json(None)

    run = guarantee_at_most_one_or_retire(decks)
    if run is None:
        return return_json(None)

    decks = league.active_decks()
    already_played = [m.opponent_deck_id for m in match.load_matches_by_deck(run)]
    run.can_play = [d.person for d in decks if d.person != person and d.id not in already_played]

    return return_json(run)

@APP.route('/api/league/drop/<person>', methods=['POST'])
def drop(person: str) -> Response:
    error = validate_api_key()
    if error:
        return error

    decks = league.active_decks_by(person)
    if len(decks) == 0:
        return return_json(generate_error('NO_ACTIVE_RUN', 'That person does not have an active run'))

    run = guarantee.exactly_one(decks)

    league.retire_deck(run)
    result = {'success':True}
    return return_json(result)

@APP.route('/api/rotation')
def rotation_api() -> Response:
    now = dtutil.now()
    diff = rotation.next_rotation() - now
    result = {
        'last': rotation.last_rotation_ex(),
        'next': rotation.next_rotation_ex(),
        'diff': diff.total_seconds(),
        'friendly_diff': dtutil.display_time(int(diff.total_seconds()))
    }
    return return_json(result)

@APP.route('/api/rotation/clear_cache')
def rotation_clear_cache() -> Response:
    rotation.clear_redis()
    rotation.rotation_redis_store()
    return return_json({'success':True})

@APP.route('/api/cards')
def cards_api() -> Response:
    blob = {'cards': cs.load_cards()}
    return return_json(blob)

@APP.route('/api/card/<card>')
def card_api(card: str) -> Response:
    return return_json(oracle.load_card(card))

@APP.route('/api/archetype/reassign', methods=['POST'])
@auth.demimod_required
@fill_form('deck_id', 'archetype_id')
def post_reassign(deck_id: int, archetype_id: int) -> Response:
    archs.assign(deck_id, archetype_id, auth.person_id())
    redis.clear(f'decksite:deck:{deck_id}')
    return return_json({'success':True, 'deck_id':deck_id})

@APP.route('/api/rule/update', methods=['POST'])
@fill_form('rule_id')
@auth.demimod_required
def post_rule_update(rule_id: int = None) -> Response:
    if rule_id is not None and request.form.get('include') is not None and request.form.get('exclude') is not None:
        inc = []
        exc = []
        for line in cast(str, request.form.get('include')).strip().splitlines():
            try:
                inc.append(parse_line(line))
            except InvalidDataException:
                return return_json({'success':False, 'msg':f"Couldn't find a card count and name on line: {line}"})
            if not cs.card_exists(inc[-1][1]):
                return return_json({'success':False, 'msg':f'Card not found in any deck: {line}'})
        for line in cast(str, request.form.get('exclude')).strip().splitlines():
            try:
                exc.append(parse_line(line))
            except InvalidDataException:
                return return_json({'success':False, 'msg':f"Couldn't find a card count and name on line: {line}"})
            if not cs.card_exists(exc[-1][1]):
                return return_json({'success':False, 'msg':f'Card not found in any deck {line}'})
        rs.update_cards(rule_id, inc, exc)
        return return_json({'success':True})
    return return_json({'success':False, 'msg':'Required keys not found'})

@APP.route('/api/sitemap/')
def sitemap() -> Response:
    urls = [url_for(rule.endpoint) for rule in APP.url_map.iter_rules() if 'GET' in rule.methods and len(rule.arguments) == 0]
    return return_json({'urls': urls})

@APP.route('/api/intro/')
def intro() -> Response:
    return return_json(not request.cookies.get('hide_intro', False) and not auth.hide_intro())

@APP.route('/api/intro/', methods=['POST'])
def hide_intro() -> Response:
    r = Response(response='')
    r.set_cookie('hide_intro', value=str(True), expires=dtutil.dt2ts(dtutil.now()) + 60 *  60 * 24 * 365 * 10)
    return r

@APP.route('/api/status/')
@auth.load_person
def person_status() -> Response:
    username = auth.mtgo_username()
    r = {
        'mtgo_username': username,
        'discord_id': auth.discord_id(),
        'admin': session.get('admin', False),
        'demimod': session.get('demimod', False),
        'hide_intro': request.cookies.get('hide_intro', False) or auth.hide_intro() or username or auth.discord_id(),
        'in_guild': session.get('in_guild', False),
        }
    if username:
        d = guarantee_at_most_one_or_retire(league.active_decks_by(username))
        if d is not None:
            r['deck'] = {'name': d.name, 'url': url_for('deck', deck_id=d.id), 'wins': d.get('wins', 0), 'losses': d.get('losses', 0)} # type: ignore
    if r['admin'] or r['demimod']:
        r['archetypes_to_tag'] = len(deck.load_decks('NOT d.reviewed'))
    active_league = league.active_league()
    if active_league:
        time_until_league_end = active_league.end_date - datetime.datetime.now(tz=datetime.timezone.utc)
        if time_until_league_end <= datetime.timedelta(days=2):
            r['league_end'] = dtutil.display_time(time_until_league_end/datetime.timedelta(seconds=1), granularity=2)
    return return_json(r)

def guarantee_at_most_one_or_retire(decks: List[Deck]) -> Optional[Deck]:
    try:
        run = guarantee.at_most_one(decks)
    except TooManyItemsException:
        league.retire_deck(decks[0])
        run = decks[1]
    return run

@APP.route('/api/admin/people/<int:person_id>/notes/')
@auth.admin_required_no_redirect
def person_notes(person_id: int) -> Response:
    return return_json({'notes': ps.load_notes(person_id)})

@APP.route('/decks/<int:deck_id>/oembed')
def deck_embed(deck_id: int) -> Response:
    # Discord doesn't actually show this yet.  I've reached out to them for better documentation about what they do/don't accept.
    d = deck.load_deck(deck_id)
    view = DeckEmbed(d, None, None)
    width = 1200
    height = 500
    embed = {
        'type': 'rich',
        'version': '1.0',
        'title': view.page_title(),
        'width': width,
        'height': height,
        'html': template.render(view)
    }
    return return_json(embed)

@APP.route('/api/test_500')
def test_500() -> Response:
    if configuration.get_bool('production'):
        return return_json(generate_error('ON_PROD', 'This only works on test environments'), status=404)
    raise TooManyItemsException()

@APP.route('/api/achievements')
def all_achievements() -> Response:
    data = {}
    data['achievements'] = [{'key': a.key, 'title': a.title, 'description': a.description_safe} for a in Achievement.all_achievements]
    return return_json(data)

@APP.route('/api/tournaments')
def all_tournaments() -> Response:
    data = {}
    data['tournaments'] = (tournaments.all_series_info())
    return return_json(data)