"""
Extract expenses from a First Direct downloaded csv, e.g. for tax self-assessment
To download the csv go to the FD online website (cannot get csv or custom date range from the mobile app)
https://www.firstdirect.com/#fd_content
select online banking from nav area before main region
enter username - email address without dots
generate security code on online device and enter on next page
select custom dates in the format prefilled in the edit field rather than trying to use date picker
download link will deliver a csv with columns for Date, Description , Amount and Balance
Save this file with a .exp extension in the folder containing the keywords and exclusions json files
run this applet and select the csv
"""
import json
import csv
import os
from typing import Any
from collections.abc import Generator, Sequence
from dialogs import Applet,TableDialog,HeteroDialog,KeyHandler, ValuesElementInterface,ValuesElement,SingleTextElement, BooleanElement, ListElement,ButtonElement
from framework import beep
def json_from_csv(csv_filename: str, json_filename: str) -> None:
    with open(csv_filename, newline="", encoding="utf-8") as f:

        reader = csv.DictReader(f)

        data = list(reader)

    with open(json_filename, "w", encoding="utf-8") as f:

        json.dump(data, f, indent=2, ensure_ascii=False)

def is_expense(transaction: dict[str, Any], keywords: Sequence[str], exclusions: Sequence[str]) -> bool:
    match=any(keyword.lower() in transaction['Description'].lower() for keyword in keywords)
    match=match and not any(exclusion_keyword.lower() in transaction['Description'].lower() for exclusion_keyword in exclusions)
    return match
def load_transactions(transactions_file: str) -> list[dict[str, Any]]:
    with open(transactions_file, 'r') as f:
        transactions = json.load(f)
    return transactions
def create_initial_json_file(contents: Any, json_file: str) -> None:
    """
    Creates an initial JSON file.
    """
    with open(json_file, 'w') as f:
        json.dump(contents, f, indent=4)
def add_to_exclusions(description: str, exclusions_file: str) -> None:
    """
    Adds a transaction to the exclusions list.
    """
    with open(exclusions_file, 'r') as f:
        exclusions = json.load(f)
    if description not in exclusions:
        exclusions.append(description)
    with open(exclusions_file, 'w') as f:
        json.dump(exclusions, f, indent=4)
def remove_from_exclusions(description: str, exclusions_file: str) -> None:
    """
    Removes a transaction from the exclusions list.
    """
    with open(exclusions_file, 'r') as f:
        exclusions = json.load(f)
    exclusions = [t for t in exclusions if t != description]
    with open(exclusions_file, 'w') as f:
        json.dump(exclusions, f, indent=4)
def extract_expenses(transactions_file: str, keywords_file: str, exclusions_file: str, expenses_file: str) -> list[dict[str, Any]] | None:
    """
    Extracts expense transactions containing any of the keywords in their descriptions.
    Excludes transactions that are in the exclusions list.
    Saves the matching transactions to the expenses JSON file.
    """
    # Load transactions
    with open(transactions_file, 'r') as f:
        transactions = json.load(f)
    # Load keywords
    with open(keywords_file, 'r') as f:
        keywords = json.load(f)
    # Load exclusions
    with open(exclusions_file, 'r') as f:
        exclusions = json.load(f)
    errors = [e for e in transactions if not (isinstance(e,dict) and all(key in e for key in ('Description','Date','Amount')))]
    if len(errors):
        #contains at least one invalid transaction
        print(f"aborting due to finding {len(errors)} invalid transactions': first invalid transaction is {errors[0]}")
        return None
    # Filter transactions that match keywords but are not in exclusions
    expenses=[t for t in transactions if is_expense(t,keywords,exclusions)]
    # Save matching transactions to the expenses JSON file
    with open(expenses_file, 'w') as f:
        json.dump(expenses, f, indent=4)
    return expenses
def add_keyword(keyword: str, keywords_file: str) -> None:
    """
    Adds a keyword to the keywords JSON file, ensuring no duplicates (case insensitive).
    """
    with open(keywords_file, 'r') as f:
        keywords = json.load(f)
    if keyword.lower() not in map(str.lower, keywords):
        keywords.append(keyword)
    with open(keywords_file, 'w') as f:
        json.dump(keywords, f, indent=4)
def remove_keyword(keyword: str, keywords_file: str) -> None:
    """
    Removes a keyword from the keywords JSON file, handling case insensitivity.
    """
    with open(keywords_file, 'r') as f:
        keywords = json.load(f)
    keywords = [k for k in keywords if k.lower() != keyword.lower()]
    with open(keywords_file, 'w') as f:
        json.dump(keywords, f, indent=4)
def convert_expenses_to_csv(expenses_file: str, csv_file: str) -> None:
    """
    Converts an expenses JSON file to a CSV file.
    """
    with open(expenses_file, 'r') as f:
        expenses = json.load(f)
    # Specify the CSV headers
    headers = ['Date', 'Description', 'Amount','Balance']
    with open(csv_file, 'w', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=headers)
        writer.writeheader()
        writer.writerows(expenses)
#class ExpensesDialog(ExplorerDialog):
    #def open_file(self,filename,safe=False):
        #if not filename.endswith('.exp'):
class ExpensesDialog(Applet,TableDialog):
    """Expense tracking dialog for analyzing bank transactions."""

    content_type: int
    transactions_file: str
    expenses_file: str
    csv_file: str
    keywords_file: str
    exclusions_file: str

    def generate_content(self, filler: str | int | None, element_type: type[ValuesElement] = ValuesElement) -> Generator[ValuesElement, None, None]:
        if filler is None:
            return
        elif isinstance(filler,str):
            self.content_type=1 #all transactions
            dirname= os.path.dirname(filler)
            root = os.path.splitext(filler)[0]
            self.transactions_file=root+'_all.json'
            json_from_csv(filler, self.transactions_file)
            self.expenses_file=root+'_expenses.json'                
            self.csv_file=root+'_expenses.csv'                                
            self.keywords_file = os.path.join(dirname,'keywords.json')
            self.exclusions_file = os.path.join(dirname,'exclusions.json')
            if not os.path.exists(self.keywords_file):
                initial_keywords = ['uber', 'bolt', 'freenow', 'hotel', 'mama shelter', 'london', 'deliveroo', 'morito', 'dishoom', 'pret a manger','white feather','gwr','trainline','apple store','coffee girl','chq','nerlins cars','sumup','zettle','google','german kraft','bay tree','paddington','it workhouse','thebull','bull inn','back in action','chatgpt', 'paypal','adobe','starlink']
                create_initial_json_file(initial_keywords,self.keywords_file)
            if not os.path.exists(self.exclusions_file):
                create_initial_json_file(['netflix','spotify','bupa'],self.exclusions_file)        
        transactions =load_transactions(self.transactions_file)
        expenses= extract_expenses(self.transactions_file, self.keywords_file, self.exclusions_file,self.expenses_file)
        if expenses is None:
            return  # Abort if extraction failed
        if isinstance(filler, int):
            #content_type = 1 for all transactions, 2 for expenses and 3 for transactions that are not expenses
            self.content_type=filler
            if filler==2:
                transactions=expenses
            elif filler==3:
                transactions = [t for t in transactions if t not in expenses]
        for transaction in transactions:
            desc = transaction['Description']
            included = 'Yes' if (transaction in expenses) else 'No'
            yield ValuesElement(self,desc,desc,values=[desc,included,transaction['Date'],str(transaction['Amount'])])
    def get_headers(self) -> list[str]:
        return(['Description','Included','Date','Amount'])

    @KeyHandler.hotkey("Control+0")
    def command_convert_to_csv(self, modified_name: str) -> None:
        convert_expenses_to_csv(self.expenses_file,self.csv_file)
        self.say_strings('converted expenses file to csv')

    @KeyHandler.hotkey("Control+1")
    def command_all_transactions(self, modified_name: str) -> None:
        self.refill(1)

    @KeyHandler.hotkey("Control+2")
    def command_expenses_transactions(self, modified_name: str) -> None:
        self.refill(2)

    @KeyHandler.hotkey("Control+3")
    def command_other_transactions(self, modified_name: str) -> None:
        self.refill(3)

    @KeyHandler.hotkey("Insert")
    def command_include(self, modified_name: str) -> None:
        description= self.current().values[0]
        add_keyword(description,self.keywords_file)
        remove_from_exclusions(description,self.exclusions_file)
        #self.say_strings(f'included {description}')
        self.refill(self.content_type,(self.cursor,0))

    @KeyHandler.hotkey("Delete")
    def command_exclude(self, modified_name: str) -> None:
        description= self.current().values[0]
        remove_keyword(description,self.keywords_file)
        add_to_exclusions(description,self.exclusions_file)
        #self.say_strings(f'excluded {description}')
        self.refill(self.content_type,(self.cursor,0))