/**
 * --------------------------------------------------------------------------------
 * FINANCIAL PORTFOLIO TRACKER (TypeScript Port)
 * --------------------------------------------------------------------------------
 * * OVERVIEW:
 * This script fetches financial trading data from a Notion database, retrieves 
 * historical price data from Yahoo Finance, calculates performance metrics 
 * (IRR, current value, dividends), and pushes the results back to Notion 
 * and a TRMNL dashboard.
 * * TYPESCRIPT CONCEPTS COVERED:
 * - Interfaces & Type Definitions
 * - Async/Await & Promises
 * - Strict Null Checks & Type Guards
 * - Optional Chaining (?.) and Nullish Coalescing (??)
 * - Type Assertions (as ...)
 * - External Module Imports
 */

// ---------------------------------------------------------
// 0. IMPORTS
// ---------------------------------------------------------

// LEARNING TIP: Default vs Named Imports
// 'yahooFinance' is a default import (export default ...).
// '{ addDays, ... }' are named imports. You must use the exact name exported by the library.
import yahooFinance from 'yahoo-finance2'; 
import { 
    addDays,
    differenceInDays,
    format,
    parseISO,
    subDays,
    isBefore,
    isAfter,
    isEqual,
    startOfDay,
    isWeekend
} from 'date-fns';

// ---------------------------------------------------------
// 1. CONFIGURATION
// ---------------------------------------------------------

// LEARNING TIP: 'as const' Assertion
// By adding `as const`, we tell TypeScript that these are READ-ONLY LITERALS.
// Instead of being type 'string', LOG_COLORS.error is literally the string '\x1b[31m'.
// This prevents accidental modification and allows us to create specific Types from keys.
const LOG_COLORS = {
    error: '\x1b[31m', // Red
    warning: '\x1b[33m', // Yellow
    success: '\x1b[32m', // Green
    info: '\x1b[90m', // Gray
    debug: '\x1b[4m', // Underscore
    endcode: '\x1b[0m' // Reset
} as const;

// LEARNING TIP: Lookup Types (keyof typeof)
// We create a new Type 'LogColorKey' that can ONLY be one of: "error" | "warning" | "success" | ...
// This gives us autocomplete support when using these keys later.
type LogColorKey = keyof typeof LOG_COLORS;

const LOGGING_LEVELS = ["none", "error", "success", "warning", "info", "debug"];

// Mutable config object (standard JavaScript object)
const config = {
    // Functionality Switches
    update_notion: false,
    update_TRMNL: true,
    calculate_benchmark: true,

    // Configuration
    programm_cooldown_time: 15, // Minutes
    api_cooldown_time: 100,     // Milliseconds
    trmnl_granularity: 80,      // Days
    ticker_benchmark: "VGWL.DE",

    // Logging
    selected_logging_level: "warning",

    // API - NOTION
    notion_token: "secret_b7PiPL2FqC9QEikqkAEWOht7LmzPMIJMWTzUPWwbw4H",
    notion_db_id_trades: "95f7a2b697a249d4892d60d855d31bda",
    notion_db_id_investments: "2ba10a5f51bd8160ab9ee982bbef8cc3",
    notion_db_id_performance: "1c010a5f51bd806f90d8e76a1286cfd4",

    // API - TRMNL
    trmnl_url_chart_1: "https://usetrmnl.com/api/custom_plugins/334ea2ed-1f20-459a-bea5-dca2c8cf7714",
    trmnl_url_chart_2: "https://usetrmnl.com/api/custom_plugins/72950759-38df-49eb-99fb-b3e2e67c385e",
    trmnl_url_chart_3: "https://usetrmnl.com/api/custom_plugins/a975543a-51dc-4793-b7fa-d6a101dc4025",
};

// Headers configuration for API requests
const notionHeaders = {
    "Authorization": "Bearer " + config.notion_token,
    "Content-Type": "application/json",
    "Notion-Version": "2022-02-22"
};

const trmnlHeaders = { "Content-Type": "application/json" };

// ---------------------------------------------------------
// 2. INTERFACES (Type Definitions)
// ---------------------------------------------------------
// LEARNING TIP: Interfaces
// Interfaces describe the "Shape" of an object. TypeScript uses "Structural Typing".
// If an object has the properties defined here, it is considered a 'Trade', even if 
// we didn't explicitly say `new Trade()`.

interface Trade {
    ticker: string;
    date_open: string;      // ISO Date string YYYY-MM-DD
    
    // LEARNING TIP: Union Types
    // 'date_close' can be a string OR the number 0. 
    // TypeScript forces us to check which one it is before using it (Type Narrowing).
    date_close: string | 0; 
    
    course_open: number;
    course_close: number;
    course_current: number;
    irr: number;
    units: number;
    dividends: number;
}

// LEARNING TIP: Index Signatures
// 'TradesDict' is an object where the Keys are strings (IDs) and Values are 'Trade' objects.
// This is used for Hash Maps / Dictionaries.
interface TradesDict {
    [key: string]: Trade;
}

interface Investment {
    ticker: string;
    total_dividends: number;
    current_value: number;
    current_irr: number;
    total_performanance: number;
}

interface InvestmentsDict {
    [key: string]: Investment;
}

interface HistoryEntry {
    current_amount: number;
    current_invested: number;
    total_dividends: number;
    current_value: number;
    current_irr: number;
    current_course: number;
    total_performanance: number;
}

interface HistoryDailyTrade {
    [tradeId: string]: HistoryEntry;
}

interface HistoryPerTrade {
    [date: string]: HistoryDailyTrade;
}

interface TickerHistoryEntry {
    current_invested: number;
    total_dividends: number;
    current_value: number;
    current_irr: number;
    total_performanance: number;
    
    // LEARNING TIP: Optional Properties (?)
    // The '?' indicates these properties might NOT exist on the object.
    // When accessing them, the result is 'number | undefined'.
    current_amount?: number; 
    current_course?: number;
    
    // Recursive Type: An entry can contain a nested benchmark entry
    benchmark?: TickerHistoryEntry; 
}

interface HistoryDailyTicker {
    [ticker: string]: TickerHistoryEntry;
}

interface HistoryPerTicker {
    [date: string]: HistoryDailyTicker;
}

// Structure for Yahoo Finance Data map
interface YFDataMap {
    [ticker: string]: {
        [date: string]: {
            close: number;
            dividends: number;
        }
    }
}

// ---------------------------------------------------------
// 3. HELPER FUNCTIONS
// ---------------------------------------------------------

/**
 * Pauses execution for a set amount of time.
 * LEARNING TIP: Promises
 * JavaScript is single-threaded. 'setTimeout' schedules code to run later.
 * We wrap it in a Promise so we can use 'await sleep(1000)' to pause execution flow.
 */
function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Prints messages to the console with colors.
 * Demonstrates default parameter values (newLine = true).
 */
function logging(message = "", loggingLevel = "", newLine = true) {
    const configLevelIdx = LOGGING_LEVELS.indexOf(config.selected_logging_level);
    let messageLevelIdx = LOGGING_LEVELS.indexOf(loggingLevel);

    if (messageLevelIdx === -1) {
        messageLevelIdx = LOGGING_LEVELS.length;
    }

    if (messageLevelIdx <= configLevelIdx) {
        // LEARNING TIP: Type Assertion (as ...)
        // We know 'loggingLevel' is a string, but we need to tell TypeScript 
        // that it matches one of the keys in LOG_COLORS to allow index access.
        const color = LOG_COLORS[loggingLevel as LogColorKey] || LOG_COLORS.info;
        const logText = `${color}[${loggingLevel}] ${LOG_COLORS.endcode}${message}`;
        
        if (newLine) {
            console.log(logText);
        } else {
            // process.stdout.write prints without a newline character at the end
            process.stdout.write(logText + " ");
        }
    }
}

/**
 * Calculates Internal Rate of Return (IRR) approximation.
 */
function calculateIrr(dateNow: Date, dateOpen: Date, valueNow: number, valueOpen: number): number {
    let irr = 0.0;
    try {
        let daysDiff = differenceInDays(dateNow, dateOpen);
        if (daysDiff === 0) daysDiff = 1; // Prevent division by zero logic

        const years = daysDiff / 365.0;
        let ratio = valueNow / valueOpen;

        // Basic Annualized Return Formula logic
        if (ratio < 0) {
            ratio = ratio * -1;
            irr = Math.pow(ratio, 1 / years);
            irr = irr * -1;
        } else {
            irr = Math.pow(ratio, 1 / years);
        }
        return irr;
    } catch (e) {
        logging("Calculation of irr failed", "error");
        return 0.0;
    }
}

/**
 * Finds the oldest date in a dictionary of trades.
 */
function getDateOpenOldestTrade(trades: TradesDict): Date {
    let oldest = new Date(); // Start with today
    for (const key in trades) {
        const trade = trades[key];
        
        // LEARNING TIP: Type Guard
        // In strict mode, dictionary access might return undefined. 
        // We check 'if (!trade) continue' to ensure 'trade' exists before using it.
        if (!trade) continue; 
        
        const d = parseISO(trade.date_open);
        // date-fns comparison function
        if (isBefore(d, oldest)) {
            oldest = d;
        }
    }
    return oldest;
}

/**
 * Extracts a unique list of ticker symbols from trades.
 */
function filterListOfTickers(trades: TradesDict): string[] {
    const tickers: string[] = [];
    try {
        for (const key in trades) {
            const trade = trades[key];
            if (!trade) continue; // Safety check

            const t = trade.ticker;
            // Only add if not already in list
            if (!tickers.includes(t)) {
                tickers.push(t);
            }
        }
        logging("", "success");
        logging(`${tickers.length} tickers found`, "info");
        return tickers;
    } catch (e) {
        logging(`${e}`, "error");
        return [];
    }
}

/**
 * Creates a list of dates (e.g., weekly) from the oldest trade to today.
 */
function createListFilteredDates(trades: TradesDict, daysSeperation: number): string[] {
    try {
        const stopDate = getDateOpenOldestTrade(trades);
        let indexDate = new Date();
        const listFilteredDates: string[] = [];

        // Loop backwards in time
        while (isAfter(indexDate, stopDate) || isEqual(indexDate, stopDate)) {
            listFilteredDates.push(format(indexDate, 'yyyy-MM-dd'));
            indexDate = subDays(indexDate, daysSeperation);
        }
        
        // Reverse to get chronological order (Oldest -> Newest)
        listFilteredDates.reverse();
        
        logging("", "success");
        logging(`${listFilteredDates.length} dates in weekly list`, "info");
        return listFilteredDates;
    } catch (e) {
        logging(`${e}`, "error");
        return [];
    }
}

function addBenchmarkTicker(tickers: string[], tickerBenchmark: string): string[] {
    if (!tickers.includes(tickerBenchmark)) {
        tickers.push(tickerBenchmark);
    }
    logging("", "success");
    return tickers;
}

/**
 * Helper to get the very last key in an object (assumes keys are sortable strings like dates).
 */
function fetchLastKeyFromDict(dict: any): string {
    const keys = Object.keys(dict).sort(); 
    
    if (keys.length === 0) {
        throw new Error("Dictionary is empty, cannot fetch last key.");
    }

    const lastKey = keys[keys.length - 1];

    // LEARNING TIP: Strict Null Checks
    // Even though logic suggests lastKey exists, TS Strict Mode demands we verify it isn't undefined.
    if (lastKey === undefined) {
        throw new Error("Last key was undefined");
    }
    
    return lastKey;
}

// ---------------------------------------------------------
// 4. NETWORK DOWNLOAD FUNCTIONS
// ---------------------------------------------------------

/**
 * Fetches pages from a Notion Database. Handles pagination automatically.
 * Returns Promise<any[]> because Notion objects are complex and we parse them later.
 */
async function notionGetPages(dbId: string, numPages: number | null = null): Promise<any[]> {
    try {
        const url = `https://api.notion.com/v1/databases/${dbId}/query`;
        const getAll = numPages === null;
        const pageSize = getAll ? 100 : numPages;
        
        let payload: any = { page_size: pageSize };
        let results: any[] = [];
        let hasMore = true;
        let nextCursor = null;

        // Loop to fetch all pages if there are more than 100
        while (hasMore) {
            if (nextCursor) {
                payload['start_cursor'] = nextCursor;
            }

            // Using standard Fetch API
            const response = await fetch(url, {
                method: 'POST',
                headers: notionHeaders as any,
                body: JSON.stringify(payload)
            });

            if (!response.ok) throw new Error(`Notion API error: ${response.status}`);
            
            const data = await response.json();
            results.push(...data.results);
            
            // Check if Notion says there are more pages
            hasMore = data.has_more && getAll;
            nextCursor = data.next_cursor;
            
            if (!getAll && results.length >= (numPages || 0)) break;
        }

        return results;
    } catch (e) {
        logging("Notion Fetch Error", "error");
        return [];
    }
}

/**
 * Fetches trades from Notion and formats them into our 'TradesDict' interface.
 */
async function fetchFormatNotionTrades(dbIdTrades: string): Promise<TradesDict> {
    const trades: TradesDict = {};
    let formatErrors = 0;
    let numberOfTrades = 0;

    // Await the asynchronous fetch
    const data = await notionGetPages(dbIdTrades);

    if (data.length === 0) return {};

    for (const page of data) {
        numberOfTrades++;
        try {
            // Extract properties from Notion's specific JSON structure
            const props = page.properties;
            
            // LEARNING TIP: Optional Chaining (?.)
            // props["Close"] might not exist. If it doesn't, ?. returns undefined immediately 
            // instead of crashing with "Cannot read property 'date' of undefined".
            let dateClose: string | 0 = 0;
            if (props["Close"]?.date?.start) {
                dateClose = props["Close"].date.start;
            }

            const dateOpen = props["Open"]?.date?.start;
            if (!dateOpen) throw new Error("Missing Open Date");

            // LEARNING TIP: Nullish Coalescing (??)
            // If the value on the left is null or undefined, use the default value on the right.
            const trade: Trade = {
                ticker: props["Ticker"]?.select?.name ?? "UNKNOWN",
                date_open: dateOpen,
                date_close: dateClose,
                course_open: props["Open (€)"]?.number ?? 0,
                course_close: props["Close (€)"]?.number ?? 0,
                course_current: props["Current (€)"]?.number ?? 0,
                irr: props["IRR (%)"]?.number ?? 0,
                units: props["Units"]?.number ?? 0,
                dividends: props["Dividends (€)"]?.number ?? 0
            };

            trades[page.id] = trade;
        } catch (e) {
            formatErrors++;
        }
    }

    if (formatErrors === 0) {
        logging("", "success");
        logging(`${numberOfTrades} trades received and formatted`, "info");
    } else {
        logging("", "warning");
        logging(`${formatErrors} trades skipped out of ${numberOfTrades}`, "warning");
    }

    return trades;
}

/**
 * Similar to above, but fetches the Investment Overview database.
 */
async function fetchFormatNotionInvestments(dbIdInvestments: string): Promise<InvestmentsDict> {
    const investments: InvestmentsDict = {};
    let formatErrors = 0;
    let numberOfInvestments = 0;

    const data = await notionGetPages(dbIdInvestments);

    for (const page of data) {
        numberOfInvestments++;
        try {
            const props = page.properties;
            investments[page.id] = {
                ticker: props["Ticker"]?.select?.name ?? "UNKNOWN",
                total_dividends: props["Dividends (€)"]?.number ?? 0,
                current_value: props["Current (€)"]?.number ?? 0,
                current_irr: props["IRR (%)"]?.number ?? 0,
                total_performanance: props["Performance (€)"]?.number ?? 0
            };
        } catch (e) {
            formatErrors++;
        }
    }

    if (formatErrors === 0) {
        logging("", "success");
        logging(`${numberOfInvestments} investments received`, "info");
    } else {
        logging("", "warning");
        logging(`${formatErrors} investments skipped`, "warning");
    }

    return investments;
}

/**
 * Fetches historical price data for all tickers from Yahoo Finance.
 */
async function fetchFormatYfData(tickers: string[]): Promise<YFDataMap> {
    const yfData: YFDataMap = {};
    let fetchErrors = 0;
    let formatErrors = 0;
    let numberOfTickers = 0;

    // LEARNING TIP: Runtime Environment Check & Dynamic Instantiation
    // Sometimes Node imports 'yahooFinance' as a Class constructor, sometimes as an Instance object,
    // depending on the exact version and module settings (CommonJS vs ESM).
    // This code checks if it's a function (Class). If so, it creates a 'new' instance.
    const yf = typeof yahooFinance === 'function' 
        ? new (yahooFinance as any)({ suppressNotices: ['ripHistorical'] }) 
        : yahooFinance;

    process.stdout.write(" ");

    for (const ticker of tickers) {
        numberOfTickers++;
        try {
            const queryOptions = { 
                period1: '2000-01-01',
                period2: format(new Date(), 'yyyy-MM-dd') 
            }; 
            
            // Await the API call
            const result = await yf.historical(ticker, queryOptions);
            
            // LEARNING TIP: Local Variable for Strict Typing
            // We build the data map in a local variable first. 
            // Writing directly to yfData[ticker][date] would cause TypeScript errors 
            // because yfData[ticker] might be undefined in the compiler's eyes.
            const tickerDataMap: { [date: string]: { close: number; dividends: number } } = {};

            // Explicitly typing 'row' as any to handle incoming data flexbility
            result.forEach((row: any) => {
                const dateStr = format(row.date, 'yyyy-MM-dd');
                
                tickerDataMap[dateStr] = {
                    close: row.close,
                    dividends: 0 
                };
            });

            // Assign the completed map to the main object
            yfData[ticker] = tickerDataMap;

        } catch (e) {
            fetchErrors++;
            // Check if 'e' is an Error object to safely access .message
            console.log(`\n[Error fetching ${ticker}]: ${e instanceof Error ? e.message : String(e)}`);
        }
        
        process.stdout.write(".");
        // Pause to avoid hitting API Rate Limits
        await sleep(config.api_cooldown_time);
    }

    process.stdout.write(" ");
    
    if (fetchErrors === 0 && formatErrors === 0) {
        logging("", "success");
        logging(`${numberOfTickers} tickers received`, "info");
    } else {
        logging("", "warning");
        logging(`${fetchErrors} fetch errors`, "warning");
    }

    return yfData;
}

// ---------------------------------------------------------
// 5. CALCULATION FUNCTIONS
// ---------------------------------------------------------

/**
 * Calculates daily performance history for every single trade.
 * Iterates day-by-day from the trade opening date to today.
 */
function calcHistoryPerTrade(trades: TradesDict, yfData: YFDataMap): HistoryPerTrade {
    const historyPerTrade: HistoryPerTrade = {};
    let missingDayEntries = 0;
    let daysFormatted = 0;
    let numberOfTrades = 0;

    const dateOpenOldestTrade = getDateOpenOldestTrade(trades);

    // Loop over every trade
    for (const tradeId in trades) {
        numberOfTrades++;
        
        // Strict check: Ensure trade exists
        const trade = trades[tradeId];
        if (!trade) continue;

        // Start from the oldest trade date across the board
        let indexDate = startOfDay(new Date(dateOpenOldestTrade));
        const today = startOfDay(new Date());
        let previousCourse = 0.0;
        
        // Accumulator for dividends specific to this trade
        let runningTotalDividends = 0.0; 

        // Determine closure date (or tomorrow if currently active)
        let dateCloseObj = (trade.date_close === 0) ? addDays(today, 1) : parseISO(trade.date_close as string);
        const dateOpenObj = parseISO(trade.date_open);

        // INNER LOOP: Iterate through every day from start to today
        while (isBefore(indexDate, addDays(today, 1))) {
            daysFormatted++;
            const indexDateIso = format(indexDate, 'yyyy-MM-dd');
            
            let currentCourse = 0.0;
            let currentDividendsPerTicker = 0.0;

            // 1. Get Price Data
            // We check if tickerData exists first to avoid undefined errors
            const tickerData = yfData[trade.ticker];
            
            if (tickerData && tickerData[indexDateIso]) {
                currentCourse = tickerData[indexDateIso].close;
                currentDividendsPerTicker = tickerData[indexDateIso].dividends;
            } else {
                // Fallback: Use previous day's price
                currentCourse = previousCourse;
                
                // FIX: Only count as "missing" if it is NOT a weekend
                if (!isWeekend(indexDate)) {
                    missingDayEntries++;
                }
            }

            // 2. Handle Closed Trades (Lock price to the closing price)
            if (isEqual(indexDate, dateCloseObj)) {
                currentCourse = trade.course_close;
            }
            previousCourse = currentCourse;

            // 3. Calculate Performance Metrics
            let currentAmount = 0;
            let currentInvested = 0;
            let currentVal = 0;
            let currentIrr = 0;
            let totalPerf = 0;

            // Only calculate if the current date is within the trade's lifespan
            if ((isAfter(indexDate, dateOpenObj) || isEqual(indexDate, dateOpenObj)) && 
                (isBefore(indexDate, dateCloseObj) || isEqual(indexDate, dateCloseObj))) {
                
                currentAmount = trade.units;
                currentInvested = currentAmount * trade.course_open;
                runningTotalDividends += currentAmount * currentDividendsPerTicker;
                currentVal = currentAmount * currentCourse;
                const currentValWithDiv = currentVal + runningTotalDividends;
                
                currentIrr = calculateIrr(indexDate, dateOpenObj, currentValWithDiv, currentInvested);
                totalPerf = currentValWithDiv - currentInvested;
            }

            // 4. Store Results in History Map
            // Initialize day object if missing
            if (!historyPerTrade[indexDateIso]) historyPerTrade[indexDateIso] = {};
            
            historyPerTrade[indexDateIso][tradeId] = {
                current_amount: currentAmount,
                current_invested: currentInvested,
                total_dividends: runningTotalDividends,
                current_value: currentVal,
                current_irr: currentIrr,
                current_course: currentCourse,
                total_performanance: totalPerf
            };

            // Increment the date (Critical to avoid infinite loops)
            indexDate = addDays(indexDate, 1);
        }
    }

    if (missingDayEntries === 0) {
        logging("", "success");
        logging(`History created with ${daysFormatted} points`, "info");
    } else {
        logging("", "warning");
        logging(`Missing YF data in ${missingDayEntries} cases (using previous day)`, "warning");
    }

    return historyPerTrade;
}

/**
 * Aggregates trade history into a per-ticker summary.
 * Combines multiple trades of the same stock into one view.
 */
function calcHistoryPerTicker(historyPerTrade: HistoryPerTrade, tickers: string[], trades: TradesDict): HistoryPerTicker {
    const historyPerTicker: HistoryPerTicker = {};
    let daysFormatted = 0;

    // Get all dates and sort them
    const dates = Object.keys(historyPerTrade).sort();

    for (const dateEntry of dates) {
        daysFormatted++;
        
        // Initialize aggregation object for this day
        const dictDaily: HistoryDailyTicker = {};
        
        // Initialize all tickers with 0 values
        tickers.forEach(t => {
            dictDaily[t] = {
                current_invested: 0,
                total_dividends: 0,
                current_value: 0,
                current_irr: 0,
                total_performanance: 0,
                current_amount: 0, 
                current_course: 0
            };
        });

        // Initialize Portfolio Total
        dictDaily["total"] = {
            current_invested: 0,
            total_dividends: 0,
            current_value: 0,
            current_irr: 0,
            total_performanance: 0
        };

        const dailyTrades = historyPerTrade[dateEntry];
        
        // Loop through all trades active on this day
        for (const tradeId in dailyTrades) {
            // Safety checks
            const tData = dailyTrades[tradeId];
            if (!tData) continue;
            
            const trade = trades[tradeId];
            if (!trade) continue; 

            const ticker = trade.ticker;
            
            if (!dictDaily[ticker]) continue; 

            const tickD = dictDaily[ticker];

            // Safely default to 0 if undefined
            const currentAmount = tickD.current_amount ?? 0;
            
            // Accumulate (Sum) Values
            tickD.current_amount = currentAmount + tData.current_amount;
            tickD.current_invested += tData.current_invested;
            tickD.total_dividends += tData.total_dividends;
            tickD.current_value += tData.current_value;
            tickD.total_performanance += tData.total_performanance;
            tickD.current_course = tData.current_course; 

            // Calculate Weighted Average IRR for this ticker
            if (tickD.current_invested === 0 && tData.current_invested === 0) {
                tickD.current_irr = 0;
            } else {
                const prevInvested = tickD.current_invested - tData.current_invested;
                const prevIrr = tickD.current_irr;
                
                if (tickD.current_invested !== 0) {
                    tickD.current_irr = (prevIrr * prevInvested + tData.current_irr * tData.current_invested) / tickD.current_invested;
                }
            }
        }

        // Calculate Portfolio Totals (Summing up tickers)
        const total = dictDaily["total"];
        for (const ticker of tickers) {
            const tData = dictDaily[ticker];
            if (!tData) continue;

            total.total_dividends += tData.total_dividends;
            total.current_value += tData.current_value;
            total.total_performanance += tData.total_performanance;

            const prevTotalInvested = total.current_invested;
            total.current_invested += tData.current_invested;

            // Weighted Average IRR for Total Portfolio
            if (total.current_invested !== 0) {
                 total.current_irr = (total.current_irr * prevTotalInvested + tData.current_irr * tData.current_invested) / total.current_invested;
            }
        }

        historyPerTicker[dateEntry] = dictDaily;
    }

    logging("", "success");
    logging(`History per ticker created`, "info");
    return historyPerTicker;
}

// ---------------------------------------------------------
// 6. BENCHMARK FUNCTIONS
// ---------------------------------------------------------

/**
 * Creates 'Shadow Trades' to simulate what would have happened if 
 * the money was invested in the benchmark ETF instead.
 */
async function createBenchmarkTrades(trades: TradesDict, yfData: YFDataMap): Promise<TradesDict> {
    const benchmarkTrades: TradesDict = {};
    let i = 0;

    for (const tradeId in trades) {
        i++;
        const original = trades[tradeId];
        if (!original) continue;

        const benchId = "benchmark" + i;

        // LEARNING TIP: Spread Operator (...)
        // Copies all properties from 'original' to the new object.
        benchmarkTrades[benchId] = { ...original };
        // Overwrite the ticker
        benchmarkTrades[benchId].ticker = config.ticker_benchmark;

        const amountInvested = original.units * original.course_open;

        // Logic to find valid trading day in YF data for open/close
        let indexDate = parseISO(original.date_open);
        let courseOpenNew = 0;
        let found = false;
        
        const benchData = yfData[config.ticker_benchmark];
        
        // Search forward for valid data (e.g. if trade happened on Sunday, find Monday's price)
        while (!found) {
            const dStr = format(indexDate, 'yyyy-MM-dd');
            if (benchData && benchData[dStr]) {
                courseOpenNew = benchData[dStr].close;
                found = true;
            } else {
                indexDate = addDays(indexDate, 1);
                // Safety break >10 days
                if (differenceInDays(indexDate, parseISO(original.date_open)) > 10) found = true; 
            }
        }

        // Avoid division by zero
        benchmarkTrades[benchId].course_open = courseOpenNew || 1; 
        benchmarkTrades[benchId].units = amountInvested / (courseOpenNew || 1);

        // Handle Close Logic if trade is closed
        if (original.date_close !== 0) {
            let closeDate = parseISO(original.date_close as string);
            found = false;
            let courseCloseNew = 0;
            while (!found) {
                const dStr = format(closeDate, 'yyyy-MM-dd');
                if (benchData && benchData[dStr]) {
                    courseCloseNew = benchData[dStr].close;
                    found = true;
                } else {
                    closeDate = addDays(closeDate, 1);
                     if (differenceInDays(closeDate, parseISO(original.date_close as string)) > 10) found = true;
                }
            }
            benchmarkTrades[benchId].course_close = courseCloseNew;
        }
    }

    logging("", "success");
    return benchmarkTrades;
}

/**
 * Merges the benchmark performance data into the main ticker history object.
 */
function mergeHistories(historyPerTicker: HistoryPerTicker, benchmarkHistory: HistoryPerTicker): HistoryPerTicker {
    const benchTicker = config.ticker_benchmark;
    let errorCount = 0;

    for (const date in historyPerTicker) {
        try {
            if (benchmarkHistory[date] && benchmarkHistory[date][benchTicker]) {
                 // LEARNING TIP: Type Casting (as any)
                 // We attach 'benchmark' to the object. TypeScript might complain if this 
                 // isn't strictly defined in the interface, so we use 'as any' to bypass the check.
                 (historyPerTicker[date] as any)["benchmark"] = benchmarkHistory[date][benchTicker];
            }
        } catch {
            errorCount++;
        }
    }

    if (errorCount === 0) logging("", "success");
    else logging(`Merge error count: ${errorCount}`, "warning");
    
    return historyPerTicker;
}

// ---------------------------------------------------------
// 7. UPLOAD & SELECT FUNCTIONS
// ---------------------------------------------------------

/**
 * Generic function to update a Notion Page.
 */
async function notionUpdatePage(pageId: string, data: any) {
    const url = `https://api.notion.com/v1/pages/${pageId}`;
    await fetch(url, {
        method: 'PATCH',
        headers: notionHeaders as any,
        body: JSON.stringify({ properties: data })
    });
}

async function pushNotionTradesUpdate(trades: TradesDict) {
    let errorCount = 0;
    process.stdout.write(" ");

    for (const id in trades) {
        try {
            const trade = trades[id];
            if (!trade) continue;

            let irrNotion = trade.irr - 1;
            irrNotion = Math.round(irrNotion * 10000) / 10000;

            const update = {
                "Current (€)": { number: trade.course_current },
                "IRR (%)": { number: irrNotion },
                "Dividends (€)": { number: trade.dividends }
            };

            await notionUpdatePage(id, update);
        } catch {
            errorCount++;
        }
        process.stdout.write(".");
        await sleep(config.api_cooldown_time);
    }
    process.stdout.write(" ");
    if(errorCount === 0) logging("", "success");
    else logging(`${errorCount} upload errors`, "warning");
}

async function pushNotionInvestmentUpdate(investments: InvestmentsDict) {
    let errorCount = 0;
    process.stdout.write(" ");

    for (const id in investments) {
        try {
            const invest = investments[id];
            if (!invest) continue;

            let irrNotion = invest.current_irr - 1;
            irrNotion = Math.round(irrNotion * 10000) / 10000;

            const update = {
                "Current (€)": { number: invest.current_value },
                "IRR (%)": { number: irrNotion },
                "Performance (€)": { number: invest.total_performanance },
                "Dividends (€)": { number: invest.total_dividends }
            };
            await notionUpdatePage(id, update);
        } catch {
            errorCount++;
        }
        process.stdout.write(".");
        await sleep(config.api_cooldown_time);
    }
    process.stdout.write(" ");
    if(errorCount === 0) logging("", "success");
    else logging(`${errorCount} upload errors`, "warning");
}

/**
 * Updates the 'trades' object with the most current calculated values from history.
 */
function selectCurrentValuePerTrade(trades: TradesDict, historyPerTrade: HistoryPerTrade): TradesDict {
    for (const id in trades) {
        try {
            const trade = trades[id];
            if (!trade) continue;

            let dateIso: string;
            // If open, use today. If closed, use close date.
            if (trade.date_close === 0) {
                dateIso = format(new Date(), 'yyyy-MM-dd');
            } else {
                dateIso = trade.date_close as string;
            }

            // Fallback if date not found
            if (!historyPerTrade[dateIso]) {
                dateIso = fetchLastKeyFromDict(historyPerTrade);
            }

            const h = historyPerTrade[dateIso]?.[id]; 
            if (h) {
                trade.course_current = h.current_course;
                trade.irr = h.current_irr;
                trade.dividends = h.total_dividends;
            }
        } catch(e) {}
    }
    logging("", "success");
    return trades;
}

function selectCurrentValuePerTicker(investments: InvestmentsDict, historyPerTicker: HistoryPerTicker): InvestmentsDict {
    const todayIso = format(new Date(), 'yyyy-MM-dd');
    
    // Determine which date key to use
    const lastKey = Object.keys(historyPerTicker).length > 0 
        ? fetchLastKeyFromDict(historyPerTicker) 
        : todayIso;

    const useDate = historyPerTicker[todayIso] ? todayIso : lastKey;

    for (const id in investments) {
        try {
            const invest = investments[id];
            if (!invest) continue;

            const ticker = invest.ticker;
            if(historyPerTicker[useDate] && historyPerTicker[useDate][ticker]) {
                const h = historyPerTicker[useDate][ticker];
                invest.total_dividends = h.total_dividends;
                invest.current_value = h.current_value;
                invest.current_irr = h.current_irr;
                invest.total_performanance = h.total_performanance;
            }
        } catch(e) {}
    }
    logging("", "success");
    return investments;
}

/**
 * Filters the detailed daily history down to specific dates (e.g. Weekly)
 * and handles data aggregation/averaging.
 */
function filterHistoryByList(history: HistoryPerTicker, datesList: string[]): HistoryPerTicker {
    const filtered: HistoryPerTicker = {};
    
    // Sort input arrays to ensure processing happens chronologically
    const sortedTargetDates = [...datesList].sort(); 
    const allHistoryDates = Object.keys(history).sort(); 
    
    let lastHistoryIdx = 0;

    for (let i = 0; i < sortedTargetDates.length; i++) {
        const targetDate = sortedTargetDates[i];
        
        // FIX 1: Strict Check - Ensure targetDate is defined before using it as an index
        if (!targetDate) continue;

        // CHECK: Is this the last date in our list (usually Today)?
        const isLastDate = i === sortedTargetDates.length - 1;

        if (isLastDate) {
            // --- LOGIC FOR TODAY (Last Entry) ---
            // We do not want to average today's data with last week's. We want the exact latest values.
            
            // Try to find exact match
            let dataToUse = history[targetDate];

            // If not found, use the very last entry in the history (fallback)
            if (!dataToUse && allHistoryDates.length > 0) {
                const lastAvailableDate = allHistoryDates[allHistoryDates.length - 1];
                if (lastAvailableDate) {
                    dataToUse = history[lastAvailableDate];
                }
            }

            if (dataToUse) {
                // Deep clone the object using JSON parse/stringify to prevent reference issues
                filtered[targetDate] = JSON.parse(JSON.stringify(dataToUse));
            }
            
        } else {
            // --- LOGIC FOR PAST DATES (Interval Averaging) ---
            
            let currentHistoryIdx = -1;
            
            // Find the index of the target date in the full history array
            for (let j = lastHistoryIdx; j < allHistoryDates.length; j++) {
                const hDate = allHistoryDates[j];
                if (hDate && hDate <= targetDate) {
                    currentHistoryIdx = j;
                } else {
                    break; // Optimization: stop searching once we pass the date
                }
            }

            if (currentHistoryIdx !== -1 && currentHistoryIdx >= lastHistoryIdx) {
                
                // Get all history days between the last target and this target
                const intervalDates = allHistoryDates.slice(lastHistoryIdx, currentHistoryIdx + 1);
                
                if (intervalDates.length > 0) {
                    
                    // Initialize Aggregation Accumulator
                    const aggregation: Record<string, {
                        wSumInvested: number; wSumValue: number; wSumPerf: number; wSumDiv: number;
                        totalWeight: number;
                        
                        wSumIrr: number; 
                        irrTotalWeight: number;
                        
                        bSumInvested: number; bSumValue: number; bSumPerf: number; bSumDiv: number;
                        bTotalWeight: number;
                        
                        bSumIrr: number;
                        bIrrTotalWeight: number;
                    }> = {};

                    // Helper to create or retrieve aggregator for a ticker
                    const getAgg = (t: string) => {
                        if (!aggregation[t]) aggregation[t] = {
                            wSumInvested: 0, wSumValue: 0, wSumPerf: 0, wSumDiv: 0, totalWeight: 0,
                            wSumIrr: 0, irrTotalWeight: 0,
                            
                            bSumInvested: 0, bSumValue: 0, bSumPerf: 0, bSumDiv: 0, bTotalWeight: 0,
                            bSumIrr: 0, bIrrTotalWeight: 0
                        };
                        return aggregation[t];
                    };

                    // Loop through every day in this interval
                    for (const date of intervalDates) {
                        if (!date) continue; // Safety check

                        const dayData = history[date];
                        if (!dayData) continue;

                        for (const ticker in dayData) {
                            const entry = dayData[ticker];
                            
                            // FIX 2: Strict Check - Ensure entry exists
                            if (!entry) continue;

                            const agg = getAgg(ticker);
                            
                            // Weight: If invested amount < 1, treat as 0 (ignore dust)
                            const w = entry.current_invested > 1 ? entry.current_invested : 0;
                            
                            // Outlier Detection: If IRR > 500% or < -500%, ignore it
                            const isOutlier = Math.abs(entry.current_irr) > 5.0; 

                            if (w > 0) {
                                // Add weighted values
                                agg.wSumInvested += entry.current_invested * w;
                                agg.wSumValue += entry.current_value * w;
                                agg.wSumPerf += entry.total_performanance * w;
                                agg.wSumDiv += entry.total_dividends * w;
                                agg.totalWeight += w;

                                // Only add IRR if sane
                                if (!isOutlier) {
                                    agg.wSumIrr += entry.current_irr * w;
                                    agg.irrTotalWeight += w;
                                }
                            }

                            // Handle Benchmark Aggregation (Same logic)
                            if (entry.benchmark) {
                                const b = entry.benchmark;
                                const bw = b.current_invested > 1 ? b.current_invested : 0;
                                const isBenchOutlier = Math.abs(b.current_irr) > 5.0;

                                if (bw > 0) {
                                    agg.bSumInvested += b.current_invested * bw;
                                    agg.bSumValue += b.current_value * bw;
                                    agg.bSumPerf += b.total_performanance * bw;
                                    agg.bSumDiv += b.total_dividends * bw;
                                    agg.bTotalWeight += bw;

                                    if (!isBenchOutlier) {
                                        agg.bSumIrr += b.current_irr * bw;
                                        agg.bIrrTotalWeight += bw;
                                    }
                                }
                            }
                        }
                    }

                    // Calculate Final Weighted Averages for the interval
                    const resultDaily: HistoryDailyTicker = {};
                    
                    for (const ticker in aggregation) {
                        const agg = aggregation[ticker];
                        
                        // FIX 3: Strict Check - Ensure agg exists
                        if (!agg) continue;

                        const div = (n: number, d: number) => d === 0 ? 0 : n / d;

                        const entry: TickerHistoryEntry = {
                            current_invested: div(agg.wSumInvested, agg.totalWeight),
                            current_value: div(agg.wSumValue, agg.totalWeight),
                            current_irr: div(agg.wSumIrr, agg.irrTotalWeight),
                            total_performanance: div(agg.wSumPerf, agg.totalWeight),
                            total_dividends: div(agg.wSumDiv, agg.totalWeight),
                            current_amount: 0,
                            current_course: 0
                        };

                        if (agg.bTotalWeight > 0) {
                            entry.benchmark = {
                                current_invested: div(agg.bSumInvested, agg.bTotalWeight),
                                current_value: div(agg.bSumValue, agg.bTotalWeight),
                                current_irr: div(agg.bSumIrr, agg.bIrrTotalWeight),
                                total_performanance: div(agg.bSumPerf, agg.bTotalWeight),
                                total_dividends: div(agg.bSumDiv, agg.bTotalWeight),
                            };
                        }

                        resultDaily[ticker] = entry;
                    }

                    filtered[targetDate] = resultDaily;
                }
                
                // Update start index for next iteration
                lastHistoryIdx = currentHistoryIdx + 1;
            }
        }
    }

    logging("", "success");
    return filtered;
}

/**
 * Formats the data for the TRMNL dashboard API.
 */
function prepTrmnlChartUpdate(history: HistoryPerTicker, s1: string = "total", d1: string = "current_value", s2: string = "benchmark", d2: string = "current_value") {
    try {
        const lastKey = fetchLastKeyFromDict(history);
        const lastEntry = history[lastKey];

        if (!lastEntry || !lastEntry["total"]) {
            throw new Error("Missing 'total' data for TRMNL update");
        }

        // Prepare Big Numbers (Top of dashboard)
        const dictBigNumbers = {
            current_value: Math.round(lastEntry["total"].current_value).toString(),
            total_performanance: Math.round(lastEntry["total"].total_performanance).toString(),
            current_irr: String(Math.round((lastEntry["total"].current_irr - 1) * 100 * 100) / 100)
        };

        const chart1Data: any[] = [];
        const chart2Data: any[] = [];

        const sortedDates = Object.keys(history).sort();

        // Convert history map to Chart Data Array
        for (const date of sortedDates) {
            const getVal = (series: string, field: string) => {
                let val = 0;
                const dailyData = history[date];
                
                // Strict check: Data must exist
                if (!dailyData) return 0;

                if (series === 'benchmark') {
                    // Safe access using Optional Chaining and Nullish Coalescing
                    val = (dailyData as any)['benchmark']?.[field] ?? 0;
                } else {
                    val = (dailyData as any)[series]?.[field] ?? 0;
                }
                
                if (field === 'current_irr') val = (val - 1) * 100;
                return Math.round(val * 100) / 100;
            };

            const val1 = getVal(s1, d1);
            const val2 = getVal(s2, d2);
            
            // Format: ISO date + Time (required by TRMNL)
            const jsonDate = date + "T00:00:00"; 

            chart1Data.push([jsonDate, val1]);
            chart2Data.push([jsonDate, val2]);
        }

        // Construct Titles
        let t1 = s1 === "total" ? "Portfolio" : s1;
        if (s1 === "benchmark") t1 = `Benchmark: ${config.ticker_benchmark}`;
        let t2 = s2 === "total" ? "Portfolio" : s2;
        if (s2 === "benchmark") t2 = `Benchmark: ${config.ticker_benchmark}`;

        const cleanName = (n: string) => n.replace("_", " ").replace(/\b\w/g, l => l.toUpperCase()).replace("Irr", "IRR");

        return {
            merge_variables: {
                big_numbers: dictBigNumbers,
                charts: [
                    { name: `${cleanName(d1)} ${t1}`, data: chart1Data },
                    { name: `${cleanName(d2)} ${t2}`, data: chart2Data }
                ]
            }
        };

    } catch (e) {
        logging(`${e}`, "error");
        return false;
    }
}

/**
 * Sends the formatted data to the TRMNL API.
 */
async function pushTrmnlUpdateChart(data: any, url: string) {
    try {
        const res = await fetch(url, {
            method: 'POST',
            headers: trmnlHeaders,
            body: JSON.stringify(data)
        });

        if (res.status === 200) logging("", "success");
        else if (res.status === 429) logging("Rate limit exceeded", "warning");
        else if (res.status === 422) logging("Payload too large/invalid", "warning");
        else logging(`TRMNL Error ${res.status}`, "error");
    } catch (e) {
        logging(`${e}`, "error");
    }
}

// ---------------------------------------------------------
// 8. MAIN EXECUTION LOOP
// ---------------------------------------------------------

/**
 * Main entry point.
 * LEARNING TIP: Async Main
 * Top-level code cannot normally use 'await' (in older Node versions).
 * Wrapping logic in an async function allows us to use await for all our API calls.
 */
async function main() {
    console.log("Starting TS-Ported Financial Service...");

    // Infinite Loop to keep service running
    while (true) {
        try {
            // --- PART 1: NOTION TRADES ---
            process.stdout.write("Fetching Data from Notion... ");
            let trades = await fetchFormatNotionTrades(config.notion_db_id_trades);

            process.stdout.write("Creating unique tickers... ");
            let tickers = filterListOfTickers(trades);

            if (config.calculate_benchmark) {
                process.stdout.write("Adding benchmark... ");
                tickers = addBenchmarkTicker(tickers, config.ticker_benchmark);
            }

            process.stdout.write("Fetching YFinance data");
            const yfData = await fetchFormatYfData(tickers);

            process.stdout.write("Calculating history per trade... ");
            const historyPerTrade = calcHistoryPerTrade(trades, yfData);

            if (config.update_notion) {
                process.stdout.write("Selecting current values... ");
                trades = selectCurrentValuePerTrade(trades, historyPerTrade);
                
                process.stdout.write("Updating Notion Trades");
                await pushNotionTradesUpdate(trades);
            }

            // --- PART 2: NOTION INVESTMENTS ---
            process.stdout.write("Fetching Notion Investments... ");
            let investments = await fetchFormatNotionInvestments(config.notion_db_id_investments);

            process.stdout.write("Calculating history per ticker... ");
            let historyPerTicker = calcHistoryPerTicker(historyPerTrade, tickers, trades);

            if (config.update_notion) {
                process.stdout.write("Calculating current ticker values... ");
                investments = selectCurrentValuePerTicker(investments, historyPerTicker);

                process.stdout.write("Updating Notion Investments");
                await pushNotionInvestmentUpdate(investments);
            }

            // --- PART 3: BENCHMARK ---
            if (config.calculate_benchmark) {
                process.stdout.write("Creating benchmark trades... ");
                const benchmarkTrades = await createBenchmarkTrades(trades, yfData);

                process.stdout.write("Calculating benchmark history per trade... ");
                const historyBenchTrade = calcHistoryPerTrade(benchmarkTrades, yfData);

                process.stdout.write("Calculating benchmark history overall... ");
                const historyBench = calcHistoryPerTicker(historyBenchTrade, tickers, benchmarkTrades);

                process.stdout.write("Merging histories... ");
                historyPerTicker = mergeHistories(historyPerTicker, historyBench);
            }

            // --- PART 4: TRMNL ---
            if (config.update_TRMNL) {
                process.stdout.write("Creating weekly dates... ");
                const datesList = createListFilteredDates(trades, config.trmnl_granularity);

                process.stdout.write("Filtering history... ");
                const historyFiltered = filterHistoryByList(historyPerTicker, datesList);

                // Chart 1: Value
                process.stdout.write("Pushing Chart 1 (Value)... ");
                const data1 = prepTrmnlChartUpdate(historyFiltered, "total", "current_value", "benchmark", "current_value");
                if(data1) await pushTrmnlUpdateChart(data1, config.trmnl_url_chart_1);

                // Chart 2: IRR
                process.stdout.write("Pushing Chart 2 (IRR)... ");
                const data2 = prepTrmnlChartUpdate(historyFiltered, "total", "current_irr", "benchmark", "current_irr");
                if(data2) await pushTrmnlUpdateChart(data2, config.trmnl_url_chart_2);

                // Chart 3: Performance
                process.stdout.write("Pushing Chart 3 (Perf)... ");
                const data3 = prepTrmnlChartUpdate(historyFiltered, "total", "total_performanance", "benchmark", "total_performanance");
                if(data3) await pushTrmnlUpdateChart(data3, config.trmnl_url_chart_3);
            }

            // --- PART 5: COOLDOWN ---
            // Clear vars for Garbage Collection
            trades = {};
            investments = {};
            
            console.log(`Completed cycle at: ${new Date().toISOString()}`);
            console.log("------------------------------------------------");
            
            // Wait before next execution
            await sleep(config.programm_cooldown_time * 60 * 1000);

        } catch (e) {
            console.error("CRITICAL MAIN LOOP ERROR:", e);
            console.log("Retrying in 1 minute...");
            await sleep(60000);
        }
    }
}

// Start the application
main();