#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright (C) 2023 Brendan Cody-Kenny
# This script is licensed under the GNU General Public License v3.0. 
# See <https://www.gnu.org/licenses/gpl-3.0.en.html> for the full license text.

# Author: Brendan Cody-Kenny
# Email: codykenny@gmail.com
# LinkedIn: https://www.linkedin.com/in/brendancodykenny

"""
This script calculates gains or losses realized for each sale transaction
First-in-first-out (FIFO) is used to match purchases with each sale
Purchases that match a sale are listed before each sale
Transactions are read from a file called degiro_transactions.csv
The full list of all transactions are required to "resolve" all sales
Example usage: python3 degiro-gain-loss-calculator.py > gains-losses-all-time.csv
"""

# Dependencies: Standard Python Library (Python 3.11)
# Version: 1.0
# Date: 2023-10-22

import csv
import sys
from collections import deque


def round_floats_in_dict(data_dict, decimal_places=3):
    """Round float values in a dictionary to the specified number of decimal places."""
    for key, value in data_dict.items():
        if isinstance(value, float):
            data_dict[key] = round(value, decimal_places)
    return data_dict


def print_dicts_to_csv(dicts_list):
    # Ensure the list isn't empty
    if not dicts_list:
        return

    rounded_data = [round_floats_in_dict(d.copy()) for d in dicts_list]

    # Create a CSV writer that writes to the console
    writer = csv.DictWriter(sys.stdout, fieldnames=dicts_list[0].keys())

    # Write the header (field names)
    writer.writeheader()

    # Write the rows
    for d in rounded_data:
        writer.writerow(d)


def compute_gain_or_loss(filename):
    with open(filename, "r") as file:
        reader = csv.DictReader(file)

        purchases = {}

        capital_gain_or_loss = 0.0

        transactions_realized = []

        for row in reversed(list(reader)):
            transaction_year = int(row["Date"].split("-")[-1])

            # Extract relevant fields
            ISIN = row["ISIN"]
            quantity = int(row["Quantity"])
            price_per_stock_in_eur = abs(float(row["Total"]) / quantity)
            t_date = row["Date"]
            t_time = row["Time"]

            if quantity > 0:  # It's a purchase
                if ISIN not in purchases:
                    purchases[ISIN] = deque()
                purchases[ISIN].append(
                    (quantity, price_per_stock_in_eur, t_date, t_time)
                )
            else:  # It's a sale
                quantity = abs(quantity)  # Make quantity positive for sales
                sale_quantity = quantity
                capital_gain_or_loss_this_sale = 0
                while quantity > 0 and purchases.get(ISIN):
                    purchase_quantity, purchase_price, p_date, p_time = purchases[
                        ISIN
                    ].popleft()

                    if purchase_quantity <= quantity:
                        capital_gain_or_loss_this_sale += (
                            price_per_stock_in_eur - purchase_price
                        ) * purchase_quantity
                        quantity -= purchase_quantity
                        transactions_realized.append(
                            {
                                "type": "purchase",
                                "isin": ISIN,
                                "date": p_date,
                                "time": p_time,
                                "quantity": purchase_quantity,
                                "total": (purchase_price) * purchase_quantity,
                                "capital_derived_from_purchase": (
                                    price_per_stock_in_eur - purchase_price
                                )
                                * purchase_quantity,
                                "total_realized_on_sale": "",
                            }
                        )
                    else:
                        capital_gain_or_loss_this_sale += (
                            price_per_stock_in_eur - purchase_price
                        ) * quantity
                        remaining_purchase_quantity = purchase_quantity - quantity
                        purchases[ISIN].appendleft(
                            (
                                remaining_purchase_quantity,
                                purchase_price,
                                p_date,
                                p_time,
                            )
                        )
                        quantity = 0
                        transactions_realized.append(
                            {
                                "type": "purchase",
                                "isin": ISIN,
                                "date": p_date,
                                "time": p_time,
                                "quantity": purchase_quantity,
                                "total": (purchase_price) * purchase_quantity,
                                "capital_derived_from_purchase": (
                                    price_per_stock_in_eur - purchase_price
                                )
                                * quantity,
                                "total_realized_on_sale": "",
                            }
                        )

                capital_gain_or_loss += capital_gain_or_loss_this_sale
                transactions_realized.append(
                    {
                        "type": "sale",
                        "isin": ISIN,
                        "date": t_date,
                        "time": t_time,
                        "quantity": sale_quantity,
                        "total": row["Total"],
                        "capital_derived_from_purchase": "",
                        "total_realized_on_sale": capital_gain_or_loss_this_sale,
                    }
                )

        print_dicts_to_csv(transactions_realized)
        return capital_gain_or_loss


filename = "degiro_transactions.csv"
gain_or_loss = compute_gain_or_loss(filename)

print(
    "Total realized capital "
    + ("gain" if gain_or_loss > 0 else "loss")
    + f" €{gain_or_loss:.2f}"
)