import os
import openpyxl
import fpdf
import argparse
import delorean
import PyPDF2
from collections import defaultdict
from sale_log import SaleLog
import matplotlib.pyplot as plt
from itertools import islice

A4_INCHES = (11.69, 8.27)


def generate_summary(logs):
    '''
    Generowanie podsumowanie zgodnie z otrzymanymi dziennikami.

    Podsumowanie zawiera:

    {
        'start_time': start_time,
        'end_time': end_time,
        'total_income': total_income,
        'average_discount': average_discount,
        'units': units,
        'by_product': <tylko wtedy, jeśli istnieje więcej niż jeden produkt.
                       Zwraca słownik {produkt: podsumowanie}>
    }

    Zauważ, że w podsumowaniu dotyczącym jednego produktu nie ma pola 'by_product'.
    '''
    total_income = sum(log.price for log in logs)
    start_time = min(log.timestamp for log in logs)
    end_time = max(log.timestamp for log in logs)
    average_discount = sum(log.discount for log in logs) / len(logs)
    units = len(logs)

    # Grupuje dane według produktów i tworzy podsumowania.
    products = defaultdict(list)
    for log in logs:
        products[log.name].append(log)

    summary = {
        'start_time': start_time,
        'end_time': end_time,
        'total_income': total_income,
        'average_discount': average_discount,
        'units': units,
    }

    if len(products) > 1:
        by_product = {name: generate_summary(logs)
                      for name, logs in products.items()}
        summary['by_product'] = by_product

    return summary


def aggregate_by_day(logs):
    '''
    Agregowanie wpisów według dni.

    Zwraca listę:
        (dzień, podsumowanie)
    '''
    days = []
    day = [logs[0]]
    for log in logs[1:]:
        end_of_day = day[0].timestamp.end_of_day
        if log.timestamp.datetime > end_of_day:
            # Nowy dzień.
            days.append(day)
            day = []
        day.append(log)

    # Generuje podsumowanie dla dnia.
    def date_string(log):
        return log.timestamp.truncate('day').datetime.strftime('%d %b')

    summaries = [
        (date_string(day[0]), generate_summary(day))
        for day in days
    ]
    return summaries


def aggregate_by_shop(logs):
    '''
    Agregowanie wpisów według sklepów.

    Zwraca listę:
        (sklep, podsumowanie)
    '''
    # Agregowanie wyników na podstawie sklepów.
    by_shop = defaultdict(list)
    for log in logs:
        by_shop[log.shop].append(log)

    # Generowanie podsumowania dla sklepów.
    summaries = [(shop, generate_summary(logs))
                 for shop, logs in by_shop.items()]
    return summaries


def graph(full_summary, products, temp_file, skip_labels=1):
    '''
    Generuje stronę z dwoma wierszami wykresów na podstawie podsumowania:
        - Górny wiersz to łączny przychód dla produktów w formie słupków warstwowych
        - Dolny wiersz to X wykresów z liczbą sztuk (po jednym dla każdego produktu)

    Na osi X znajdują się nazwy z podsumowania.

    full_summary: lista [(nazwa, podsumowanie)]
    products: wszystkie dostępne produkty
    temp_file: nazwa pliku tymczasowego do zapisywania pliku PDF z wykresami
    skip_labels: opcjonalna wartość powodująca pomijanie etykiet; pozwala poprawić
                 czytelność; ustawienie domyślne to 1 (wyświetlanie wszystkich etykiet)
    '''
    pos = list(range(len(full_summary)))
    units = [summary['units'] for tag, summary in full_summary]
    # Obliczanie średniego rabatu
    # discount = [summary['average_discount'] for tag, summary in full_summary]

    income_by_product = []
    units_per_product = []
    # Zapisywanie zagregowanego przychodów na potrzeby wyświetlania słupków.
    baselevel = None
    default = {
        'total_income': 0,
        'units': 0,
    }
    max_units = 0
    for product in products:
        product_income = [
            summary['by_product'].get(product, default)['total_income']
            for day, summary in full_summary
        ]
        product_units = [summary['by_product'].get(product, default)['units']
                         for day, summary in full_summary]
        if not baselevel:
            baselevel = [0 for _ in range(len(full_summary))]

        income_by_product.append((product, product_income, baselevel))
        units_per_product.append((product, product_units))
        max_units = max(max(product_units), max_units)

        baselevel = [product + bottom
                     for product, bottom in zip(product_income, baselevel)]

    labels = [day for day, summary in full_summary]

    plt.figure(figsize=A4_INCHES)

    plt.subplot(2, 1, 1)
    plt.ylabel('Przychód z podziałem na produkty')
    for name, product, baseline in income_by_product:
        plt.bar(pos, product, bottom=baseline)

    plt.legend([name for name, _, _ in income_by_product])

    # Wyświetlanie linii ze średnim rabatem.
    # plt.twinx()
    # plt.plot(pos, discount, 'o-', color='green')
    # plt.ylabel('Średni rabat')

    plt.xticks(pos[::skip_labels], labels[::skip_labels])

    max_units += 1

    num_products = len(units_per_product)
    for index, (product, units) in enumerate(units_per_product):
        plt.subplot(2, num_products, num_products + index + 1)
        plt.ylabel('Sprzedaż produktu {}'.format(product))
        plt.ylim(ymax=max_units)
        plt.bar(pos, units)
        # Wyświetlanie tylko wybranych etykiet.
        plt.xticks(pos[::skip_labels], labels[::skip_labels])

    plt.savefig(temp_file)
    return temp_file


def create_summary_brief(summary, temp_file):
    '''
    Zapisuje stronę pliku PDF z podsumowaniem w podanym pliku temp_file.
    '''
    document = fpdf.FPDF()
    document.set_font('Times', '', 12)
    document.add_page()
    TEMPLATE = '''
    Data wygenerowania raportu: {now}
    Dotyczy okresu od {start_time} do {end_time}


    Podsumowanie
    -------
    PRZYCHOD W SUMIE: {income} (w dolarach)
    SPRZEDANYCH JEDNOSTEK: {units}
    SREDNI RABAT: {discount}%
    '''

    def format_full_tmp(timestamp):
        return timestamp.datetime.isoformat()

    def format_brief_tmp(timestamp):
        return timestamp.datetime.strftime('%d %b')

    text = TEMPLATE.format(now=format_full_tmp(delorean.utcnow()),
                           start_time=format_brief_tmp(summary['start_time']),
                           end_time=format_brief_tmp(summary['end_time']),
                           income=summary['total_income'],
                           units=summary['units'],
                           discount=summary['average_discount'])

    document.multi_cell(0, 6, text)
    document.ln()
    document.output(temp_file)
    return temp_file


def main(input_file, output_file):
    xlsfile = openpyxl.load_workbook(input_file)
    sheet = xlsfile['Sheet']

    def row_to_dict(header, row):
        return {header: cell.value for cell, header in zip(row, header)}

    # islice powoduje pominięcie pierwszego wiersza (nagłówka).
    data = [SaleLog.from_row([cell.value for cell in row])
            for row in islice(sheet, 1, None)]

    # Generowanie wszystkich stron: pełne podsumowanie, wykres na podstawie
    # dni i wykres na podstawie sklepów.
    total_summary = generate_summary(data)
    products = total_summary['by_product'].keys()
    summary_by_day = aggregate_by_day(data)
    summary_by_shop = aggregate_by_shop(data)

    # Tworzenie pliku PDF z krótkim podsumowaniem i wszystkimi wykresami.
    summary_file = create_summary_brief(total_summary, 'summary.pdf')
    by_day_file = graph(summary_by_day, products, 'by_day.pdf', 7)
    by_shop_file = graph(summary_by_shop, products, 'by_shop.pdf')

    # Grupowanie wszystkich plików PDF w jednym pliku.
    pdfs = [summary_file, by_day_file, by_shop_file]
    pdf_files = [open(filename, 'rb') for filename in pdfs]
    output_pdf = PyPDF2.PdfFileWriter()
    for pdf in pdf_files:
        reader = PyPDF2.PdfFileReader(pdf)
        output_pdf.appendPagesFromReader(reader)

    # Zapisywanie wynikowego pliku PDF.
    with open(output_file, "wb") as out_file:
        output_pdf.write(out_file)

    # Zamykanie plików.
    for pdf in pdf_files:
        pdf.close()

    # Porządkowanie plików tymczasowych.
    for pdf_filename in pdfs:
        os.remove(pdf_filename)


if __name__ == '__main__':
    # Pobieranie nazw plików wejściowego i wyjściowego z wiersza poleceń.
    parser = argparse.ArgumentParser()
    parser.add_argument(type=str, dest='input_file')
    parser.add_argument(type=str, dest='output_file')
    args = parser.parse_args()

    # Wywołanie funkcji main.
    main(args.input_file, args.output_file)
