import chalk from 'chalk'
import path from 'path';
import fs from 'fs/promises';
import { pathToFileURL } from 'url';
import parse from "csv-parser"
import Node from "../model/Node.js"

export default class Database {
    #nodes;
    #edges;
    #labels = {}

    constructor() {
        this.getData()
    }

    async getData() {
        const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
        let dbJson;
        try {
            dbJson = JSON.parse(dbData);
        } catch {
            dbJson = []
        }
        this.#nodes = dbJson["nodes"];
        this.#edges = dbJson["edges"];
    
        console.log(chalk.yellow("DB established."))
        Object.preventExtensions(this);
    }

    // superKey = "nodes" || "edges"
    async writeData(superKey, key, value) {
        const dbData = await fs.readFile(path.join(process.cwd(), 'db/db.json'), 'utf8');
        let dbJson;
        try {
            dbJson = JSON.parse(dbData);
        } catch {
            dbJson = []
        }

        dbJson[superKey][key] = value;

        await fs.writeFile(path.join(process.cwd(), 'db/db.json'), JSON.stringify(dbJson, null, 2), 'utf8')
    }

    async getLabelModels() {
        const labelHandlers = {};
        const labelDir = path.join(process.cwd(), 'src/model/labels');
        const files = await fs.readdir(labelDir);
    
        for (const file of files) {
            if (!file.endsWith('.js')) continue;
    
            const label = path.basename(file, '.js');
            const modulePath = path.join(labelDir, file);
            const module = await import(pathToFileURL(modulePath).href);
            labelHandlers[label] = module.default;
    
            if (!this.#labels[label]) {
                this.#labels[label] = [];
            }
        }
        return labelHandlers
    }

    generateUserID() {
        let id = this.#labels["User"].length + 1;
        while (this.get.user(`user-${id}`)) {
            id++;
        }
        return `user-${id}`; // O(1) most of the time
    }

    async getAll() {
        return { nodes: this.#nodes }
    }

    async loadCSV(csvString) {
        if (!csvString || typeof csvString !== 'string') {
            throw new Error('loadCSVAsync: input must be a non-empty string');
        }

        return new Promise((resolve, reject) => {
            const records = [];

            const parser = parse({
                columns: true,
                skip_empty_lines: true,
                trim: true,
                relax_column_count: true,
                bom: true
            });

            parser.on('readable', () => {
                let row;
                while ((row = parser.read()) !== null) {
                    records.push(this.cleanRow(row));
                }
            });

            parser.on('error', (err) => {
                reject(err);
            });

            parser.on('end', () => {
                resolve(records);
            });

            // Feed the CSV string
            parser.write(csvString);
            parser.end();
        });
    }

    cleanRow(row) {
        const cleaned = {};

        for (let [key, value] of Object.entries(row)) {
            // 1. Clean header
            key = key
            .replace(/ -MyData/g, '')
            .replace(/[^a-zA-Z0-9]/g, ' ')
            .trim()
            .replace(/\s+(.)/g, (_, c) => c.toUpperCase())
            .replace(/^(.)/, c => c.toLowerCase());

            // 2. Empty → null
            if (value === '' || value == null) {
            cleaned[key] = null;
            continue;
            }

            // 3. Type conversion
            if (key === 'registrationStatus') {
            cleaned[key] = value === 'True' ? true : value === 'False' ? false : null;
            } 
            else if (['confidences', 'personId', 'uid', 'stateVoterId'].includes(key)) {
            const num = parseFloat(value);
            cleaned[key] = isNaN(num) ? null : num;
            } 
            else {
            cleaned[key] = value;
            }
        }

        return cleaned;
    }
}