import csv
import enum
import uuid
import time
import datetime
from collections import OrderedDict
from io import StringIO
from typing import Callable, Iterable, Tuple

import boltons
import flask
import flask_weasyprint
import teal.marshmallow
from boltons import urlutils
from flask import make_response, g, request
from flask import current_app as app
from flask.json import jsonify
from teal.cache import cache
from teal.resource import Resource, View

from ereuse_devicehub import auth
from ereuse_devicehub.db import db
from ereuse_devicehub.resources.enums import SessionType
from ereuse_devicehub.resources.user.models import Session
from ereuse_devicehub.resources.action import models as evs
from ereuse_devicehub.resources.device import models as devs
from ereuse_devicehub.resources.deliverynote.models import Deliverynote
from ereuse_devicehub.resources.device.views import DeviceView
from ereuse_devicehub.resources.documents.device_row import (DeviceRow, StockRow, ActionRow,
                                                             InternalStatsRow)
from ereuse_devicehub.resources.lot import LotView
from ereuse_devicehub.resources.lot.models import Lot
from ereuse_devicehub.resources.action.models import Trade
from ereuse_devicehub.resources.device.models import Device
from ereuse_devicehub.resources.hash_reports import insert_hash, ReportHash, verify_hash


class Format(enum.Enum):
    HTML = 'HTML'
    PDF = 'PDF'


class DocumentView(DeviceView):
    class FindArgs(DeviceView.FindArgs):
        format = teal.marshmallow.EnumField(Format, missing=None)

    def get(self, id):
        """Get a collection of resources or a specific one.
        ---
        parameters:
        - name: id
          in: path
          description: The identifier of the resource.
          type: string
          required: false
        responses:
          200:
            description: Return the collection or the specific one.
        """
        args = self.QUERY_PARSER.parse(self.find_args,
                                       flask.request,
                                       locations=('querystring',))
        if id:
            # todo we assume we can pass both device id and action id
            # for certificates... how is it going to end up being?
            try:
                id = uuid.UUID(id)
            except ValueError:
                try:
                    id = int(id)
                except ValueError:
                    raise teal.marshmallow.ValidationError('Document must be an ID or UUID.')
                else:
                    query = devs.Device.query.filter_by(id=id)
            else:
                query = evs.Action.query.filter_by(id=id)
        else:
            flask.current_app.auth.requires_auth(lambda: None)()  # todo not nice
            query = self.query(args)

        type = urlutils.URL(flask.request.url).path_parts[-2]
        if type == 'erasures':
            template = self.erasure(query)
        if args.get('format') == Format.PDF:
            res = flask_weasyprint.render_pdf(
                flask_weasyprint.HTML(string=template), download_filename='{}.pdf'.format(type)
            )
            insert_hash(res.data)
        else:
            res = flask.make_response(template)
        return res

    @staticmethod
    def erasure(query: db.Query):
        def erasures():
            for model in query:
                if isinstance(model, devs.Computer):
                    for erasure in model.privacy:
                        yield erasure
                elif isinstance(model, devs.DataStorage):
                    erasure = model.privacy
                    if erasure:
                        yield erasure
                else:
                    assert isinstance(model, evs.EraseBasic)
                    yield model

        url_pdf = boltons.urlutils.URL(flask.request.url)
        url_pdf.query_params['format'] = 'PDF'
        params = {
            'title': 'Erasure Certificate',
            'erasures': tuple(erasures()),
            'url_pdf': url_pdf.to_text()
        }
        return flask.render_template('documents/erasure.html', **params)


class DevicesDocumentView(DeviceView):
    @cache(datetime.timedelta(minutes=1))
    def find(self, args: dict):
        query = (x for x in self.query(args) if x.owner_id == g.user.id)
        return self.generate_post_csv(query)

    def generate_post_csv(self, query):
        """Get device query and put information in csv format."""
        data = StringIO()
        cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
        first = True
        document_ids = self.get_documents_id()
        for device in query:
            d = DeviceRow(device, document_ids)
            if first:
                cw.writerow(d.keys())
                first = False
            cw.writerow(d.values())
        bfile = data.getvalue().encode('utf-8')
        output = make_response(bfile)
        insert_hash(bfile)
        output.headers['Content-Disposition'] = 'attachment; filename=export.csv'
        output.headers['Content-type'] = 'text/csv'
        return output

    def get_documents_id(self):
        # documentIds = {dev_id: document_id, ...}
        deliverys = Deliverynote.query.all()
        documentIds = {x.id: d.document_id for d in deliverys for x in d.lot.devices}
        return documentIds


class ActionsDocumentView(DeviceView):
    @cache(datetime.timedelta(minutes=1))
    def find(self, args: dict):
        query = (x for x in self.query(args))
        return self.generate_post_csv(query)

    def generate_post_csv(self, query):
        """Get device query and put information in csv format."""
        data = StringIO()
        cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
        first = True
        devs_id = []
        for device in query:
            devs_id.append(device.id)
            for allocate in device.get_metrics():
                d = ActionRow(allocate)
                if first:
                    cw.writerow(d.keys())
                    first = False
                cw.writerow(d.values())
        query_trade = Trade.query.filter(Trade.devices.any(Device.id.in_(devs_id))).all()

        for trade in query_trade:
            data_rows = trade.get_metrics()
            for row in data_rows:
                d = ActionRow(row)
                if first:
                    cw.writerow(d.keys())
                    first = False
                cw.writerow(d.values())

        bfile = data.getvalue().encode('utf-8')
        output = make_response(bfile)
        insert_hash(bfile)
        output.headers['Content-Disposition'] = 'attachment; filename=actions_export.csv'
        output.headers['Content-type'] = 'text/csv'
        return output


class LotsDocumentView(LotView):
    def find(self, args: dict):
        query = (x for x in self.query(args) if x.owner_id == g.user.id)
        return self.generate_lots_csv(query)

    def generate_lots_csv(self, query):
        """Get lot query and put information in csv format."""
        data = StringIO()
        cw = csv.writer(data)
        first = True
        for lot in query:
            _lot = LotRow(lot)
            if first:
                cw.writerow(_lot.keys())
                first = False
            cw.writerow(_lot.values())
        bfile = data.getvalue().encode('utf-8')
        output = make_response(bfile)
        insert_hash(bfile)
        output.headers['Content-Disposition'] = 'attachment; filename=lots-info.csv'
        output.headers['Content-type'] = 'text/csv'
        return output


class LotRow(OrderedDict):
    def __init__(self, lot: Lot) -> None:
        super().__init__()
        self.lot = lot
        # General information about lot
        self['Id'] = lot.id.hex
        self['Name'] = lot.name
        self['Registered in'] = format(lot.created, '%c')
        try:
            self['Description'] = lot.description
        except:
            self['Description'] = ''


class StockDocumentView(DeviceView):
    # @cache(datetime.timedelta(minutes=1))
    def find(self, args: dict):
        query = (x for x in self.query(args) if x.owner_id == g.user.id)
        return self.generate_post_csv(query)

    def generate_post_csv(self, query):
        """Get device query and put information in csv format."""
        data = StringIO()
        cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
        first = True
        for device in query:
            d = StockRow(device)
            if first:
                cw.writerow(d.keys())
                first = False
            cw.writerow(d.values())
        bfile = data.getvalue().encode('utf-8')
        output = make_response(bfile)
        insert_hash(bfile)
        output.headers['Content-Disposition'] = 'attachment; filename=devices-stock.csv'
        output.headers['Content-type'] = 'text/csv'
        return output


class CheckView(View):
    model = ReportHash

    def get(self):
        qry = dict(request.values)
        hash3 = qry.get('hash')

        result = False
        if hash3 and ReportHash.query.filter_by(hash3=hash3).count():
            result = True
        return jsonify(result)


class StampsView(View):
    """
    This view render one public ans static page for see the links for to do the check
    of one csv file
    """
    def get_url_path(self):
        url = urlutils.URL(request.url)
        url.normalize()
        url.path_parts = url.path_parts[:-2] + ['check', '']
        return url.to_text()

    def get(self):
        result = ('', '')
        return flask.render_template('documents/stamp.html', rq_url=self.get_url_path(),
                result=result)

    def post(self):
        result = ('', '')
        if 'docUpload' in request.files:
            file_check = request.files['docUpload']
            bad = 'There are no coincidences. The attached file data does not come \
                   from our backend or it has been subsequently modified.'
            ok = '100% coincidence. The attached file contains data 100% existing in \
                  to our backend'
            result = ('Bad', bad)
            mime = ['text/csv', 'application/pdf', 'text/plain', 'text/markdown',
                    'image/jpeg', 'image/png', 'text/html',
                    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                    'application/vnd.oasis.opendocument.spreadsheet',
                    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                    'application/msword']
            if file_check.mimetype in mime:
                if verify_hash(file_check):
                    result = ('Ok', ok)

        return flask.render_template('documents/stamp.html', rq_url=self.get_url_path(),
                result=result)


class InternalStatsView(DeviceView):
    @cache(datetime.timedelta(minutes=1))
    def find(self, args: dict):
        if not g.user.email == app.config['EMAIL_ADMIN']:
            return jsonify('')
        query = evs.Action.query.filter(
            evs.Action.type.in_(('Snapshot', 'Live', 'Allocate', 'Deallocate')))
        return self.generate_post_csv(query)

    def generate_post_csv(self, query):
        d = {}
        for ac in query:
            create = '{}-{}'.format(ac.created.year, ac.created.month)
            user = ac.author.email

            if user not in d:
                d[user] = {}
            if create not in d[user]:
                d[user][create] = []
            d[user][create].append(ac)

        data = StringIO()
        cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"')
        cw.writerow(InternalStatsRow('', "2000-1", []).keys())
        for user, createds in d.items():
            for create, actions in createds.items():
                cw.writerow(InternalStatsRow(user, create, actions).values())

        bfile = data.getvalue().encode('utf-8')
        output = make_response(bfile)
        insert_hash(bfile)
        output.headers['Content-Disposition'] = 'attachment; filename=internal-stats.csv'
        output.headers['Content-type'] = 'text/csv'
        return output


class WbConfDocumentView(DeviceView):
    def get(self, wbtype: str):
        if not wbtype.lower() in ['usodyrate', 'usodywipe']:
            return jsonify('')

        data = {'token': self.get_token(),
                'host': app.config['HOST'],
                'inventory': app.config['SCHEMA']
                }
        data['erase'] = False
        # data['erase'] = True if wbtype == 'usodywipe' else False

        env = flask.render_template('documents/wbSettings.ini', **data)
        output = make_response(env)
        output.headers['Content-Disposition'] = 'attachment; filename=settings.ini'
        output.headers['Content-type'] = 'text/plain'
        return output

    def get_token(self):
        if not g.user.sessions:
            ses = Session(user=g.user)
            db.session.add(ses)
            db.session.commit()

        tk = ''
        now = time.time()
        for s in g.user.sessions:
            if s.type == SessionType.Internal and (s.expired == 0 or s.expired > now):
                tk = s.token
                break

        assert tk != ''

        token = auth.Auth.encode(tk)
        return token


class DocumentDef(Resource):
    __type__ = 'Document'
    SCHEMA = None
    VIEW = None  # We do not want to create default / documents endpoint
    AUTH = False

    def __init__(self, app,
                 import_name=__name__,
                 static_folder='static',
                 static_url_path=None,
                 template_folder='templates',
                 url_prefix=None,
                 subdomain=None,
                 url_defaults=None,
                 root_path=None,
                 cli_commands: Iterable[Tuple[Callable, str or None]] = tuple()):
        super().__init__(app, import_name, static_folder, static_url_path, template_folder,
                         url_prefix, subdomain, url_defaults, root_path, cli_commands)
        d = {'id': None}
        get = {'GET'}

        view = DocumentView.as_view('main', definition=self, auth=app.auth)

        # TODO @cayop This two lines never pass
        if self.AUTH:
            view = app.auth.requires_auth(view)

        self.add_url_rule('/erasures/', defaults=d, view_func=view, methods=get)
        self.add_url_rule('/erasures/<{}:{}>'.format(self.ID_CONVERTER.value, self.ID_NAME),
                          view_func=view, methods=get)

        devices_view = DevicesDocumentView.as_view('devicesDocumentView',
                                                   definition=self,
                                                   auth=app.auth)
        devices_view = app.auth.requires_auth(devices_view)

        stock_view = StockDocumentView.as_view('stockDocumentView', definition=self)
        stock_view = app.auth.requires_auth(stock_view)

        self.add_url_rule('/devices/', defaults=d, view_func=devices_view, methods=get)

        lots_view = LotsDocumentView.as_view('lotsDocumentView', definition=self)
        lots_view = app.auth.requires_auth(lots_view)
        self.add_url_rule('/lots/', defaults=d, view_func=lots_view, methods=get)

        stock_view = StockDocumentView.as_view('stockDocumentView', definition=self, auth=app.auth)
        stock_view = app.auth.requires_auth(stock_view)
        self.add_url_rule('/stock/', defaults=d, view_func=stock_view, methods=get)

        check_view = CheckView.as_view('CheckView', definition=self, auth=app.auth)
        self.add_url_rule('/check/', defaults={}, view_func=check_view, methods=get)

        stamps_view = StampsView.as_view('StampsView', definition=self, auth=app.auth)
        self.add_url_rule('/stamps/', defaults={}, view_func=stamps_view, methods={'GET', 'POST'})

        internalstats_view = InternalStatsView.as_view(
            'InternalStatsView', definition=self, auth=app.auth)
        internalstats_view = app.auth.requires_auth(internalstats_view)
        self.add_url_rule('/internalstats/', defaults=d, view_func=internalstats_view,
                          methods=get)

        actions_view = ActionsDocumentView.as_view('ActionsDocumentView',
                                                   definition=self,
                                                   auth=app.auth)
        actions_view = app.auth.requires_auth(actions_view)
        self.add_url_rule('/actions/', defaults=d, view_func=actions_view, methods=get)

        wbconf_view = WbConfDocumentView.as_view('WbConfDocumentView',
                                                  definition=self,
                                                  auth=app.auth)
        wbconf_view = app.auth.requires_auth(wbconf_view)
        self.add_url_rule('/wbconf/<string:wbtype>', view_func=wbconf_view, methods=get)