import blessed from 'blessed';
import contrib from 'blessed-contrib';
import clipboard from 'clipboardy';

/**
 * Une classe pour afficher un objet JSON de manière interactive en plein écran.
 * Permet la navigation, la recherche et le "zoom" sur des objets liés.
 */
export class JSONViewer extends blessed.box {
    /**
     * @param {object} options Options de la classe.
     * @param {object} options.obj L'objet JSON initial à afficher.
     * @param {function} options.fetchObject Fonction de rappel pour récupérer un objet JSON à partir d'un identifiant.
     * @param {object} options.screen L'écran `blessed` parent.
     * @param {string} options.context Le contexte de l'objet affiché (user, company)
     * @param {object} options.reqoptions Les options de la requête (pour la pagination sur des get users ou companies)
     */
    constructor(options) {
        super({
            parent: options.screen,
            top: '10%',
            left: 0,
            width: '100%',
            height: '80%',
            tags: true,
            border: { type: 'line' },
            label: 'Object browser',
            style: {
                fg: 'white',
                bg: 'black',
                border: { fg: 'cyan' }
            }
        });

        this.screen = options.screen;
        this.fetchObject = options.fetchObject;
        this.context = options.context;
        this.reqoptions = options.reqoptions;
        this.history = []; // Pile pour l'historique de navigation (zoom)
        this.objectCache = {};
        this.idKeys = ['userId', 'companyId', 'applicationId', 'ownerId', 'id'];

        this.pathList = contrib.tree({
            parent: options.screen,
            top: 0,
            left: 0,
            width: '100%',
            height: '10%',
            border: { type: 'line' },
            style: {
                selected: { bg: 'blue' }
            },
            //keys: true,
            vi: true,
            mouse: true
        });

        this.tree = contrib.tree({
            parent: this,
            top: 0,
            left: 0,
            width: '95%',
            height: '95%',
            //keys: true,
            vi: true,
            mouse: true,
            tags: true,
            style: {
                selected: { bg: 'blue' }
            },
            scrollbar: {
                ch: ' ',
                track: {
                    bg: 'yellow'
                },
                style: {
                    inverse: true
                }
            }
        });

        this.searchInput = blessed.textbox({
            parent: this,
            bottom: 0,
            left: 0,
            width: '100%',
            height: 3,
            inputOnFocus: true,
            hidden: true,
            style: { fg: 'white', bg: 'blue' },
            label: ' Recherche (Entrée pour valider, Échap pour annuler) '
        });

        this.loggerBox = blessed.log({
            parent: options.screen,
            top: '90%',
            left: 0,
            width: '100%',
            height: '10%',
            border: { type: 'line' },
            tags: true,
            keys: true,
            vi: true,
            mouse: true,
            label: 'Log',
            scrollback: 100,
            scrollbar: {
                ch: ' ',
                track: {
                    bg: 'yellow'
                },
                style: {
                    inverse: true
                }
            }
        });

        // Crée une boîte d'affichage de l'aide
        this.helpBox = blessed.box({
            parent: options.screen,
            top: 'center',
            left: 'center',
            width: '50%',
            height: '50%',
            align: 'left',
            valign: 'middle',
            border: 'line',
            tags: true,
            style: {
                fg: 'white',
                bg: 'black',
                border: {
                    fg: 'white'
                }
            },
            content: `
                    {bold}Help{/bold}
                    --------------------------------------
        
                    {bold}Navigation{/bold}
                    - Up / Down: move into the tree
                    - PgUp / PgDown: Display next / previous page of data for paginated result
                    - Space / ENTER: Open / Close a branch (when current node is navigable, ENTER displays the child node)
        
                    {bold}Selection{/bold}
                    - C-C : Copy current value to clipboard

                    {bold}Search{/bold}
                    - / : Open search
                    - ENTER: Submit search
                    - n: Go to next result
                    - ESC: Cancel search and close search window
        
                    {bold}General{/bold}
                    - F1 or ? : Display / hide this help window
                    - ESC / q / C-C : Quit application
                    `,
            label: 'Help (ESC to hide)',
            hidden: true
        });

        this.searchKeyword = '';
        this.lastSearchIndex = -1;
        this.flatNodeList = [];
        this.objectStack = [];

        this.initEventListeners();
        this.displayObject(options.obj, options.context);
    }

    // --- Méthodes internes de la classe ---

    initEventListeners() {
        // Gère la navigation dans l'arbre avec les flèches et la souris
        this.tree.rows.on('keypress', async (ch, key) => {
            //this.loggerBox.log(`keypress ${this.context}`);
            //this.loggerBox.log(key);
            /*
            if (key.name === 'up' || key.name === 'down') {
                process.nextTick(() => this.updatePath());
            }
            */
            if (this.context === "users" || this.context === "companies" || this.context === "applications") {
                // Requires pagination support. Object has properties data.limit, data.offset, data.total
                if ((key.name === 'pagedown') || (key.name === 'pageup')) {
                    const currentPage = (this.currentObject.offset / this.currentObject.limit) + 1;
                    const maxPage = Math.ceil(this.currentObject.total / this.currentObject.limit);
                    const newPage = (key.name === 'pagedown' ? currentPage + 1 : currentPage - 1);
                    //this.loggerBox.log(`currentPage: [${currentPage}] maxPage: [${maxPage}] newPage: [${newPage}]`);

                    let newObject;
                    if ((0 < newPage) && (newPage <= maxPage)) {
                        this.loggerBox.log(`Loading ${this.context} page ${newPage}/${maxPage}...`);
                        this.reqoptions.page = newPage;
                        const loading = blessed.loading({
                            parent: this.tree
                        });
                        loading.load('Please wait...');
                        newObject = await this.fetchObject('', '', this.context, this.reqoptions);
                        loading.stop();
                    }
                    if (newObject) {
                        this.displayObject(newObject, this.context);
                    }
                }
            }
        });

        /*
        this.tree.rows.on('mouse', (data) => {
            if (data.action === 'mousedown') {
                this.loggerBox.log('mousedown');
                process.nextTick(() => this.updatePath());
            }
        });
        */

        // Gère la sélection d'un identifiant pour "zoomer"
        this.tree.on('select', async (node) => {
            //this.loggerBox.log('select');
            const key = node.name.split(':')[0].trim();
            const value = node.name.split(':')[1] ? node.name.split(':')[1].replace(/['"]+/g, '').trim() : null;

            if (this.idKeys.includes(key) && value) {
                //this.loggerBox.log(`key ${key} ${value} navigable`);
                let newObject;
                if (this.objectCache[value]) newObject = this.objectCache[value];
                else {
                    newObject = await this.fetchObject(key, value, this.context);
                    this.objectCache[value] = newObject;
                }
                if (newObject) {
                    //this.loggerBox.log('newObject');
                    this.objectStack.push({ obj: this.currentObject, selectedNode: this.tree.rows.selected });
                    this.displayObject(newObject);
                }
            }
        });

        /*
        // Gère la navigation entre les widgets avec Tab
        this.screen.key('tab', () => {
            //this.loggerBox.log('tab');
            if (this.screen.focused === this.tree) {
                this.pathList.focus();
            } else {
                this.tree.focus();
            }
        });
        */

        // Copy current value to clipboard
        this.screen.key('C-c', () => {
            const node = this.tree.rows.items[this.tree.rows.selected];
            if (node) {
                //this.loggerBox.log(node.content);
                //const key = node.content.split(':')[0].trim();
                const value = node.content.split(':')[1] ? node.content.split(':')[1].replace(/['"]+/g, '').trim() : null;

                clipboard.writeSync(value);
                this.loggerBox.log(`value '${value}' copied to clipboard`);
            }
            else {
                this.loggerBox.log(`no node for ${this.tree.rows.selected}`);
            }
        });

        // Gère l'affichage de la fenêtre d'aide
        this.screen.key(['f1', '?'], () => {
            if (this.helpBox.hidden) {
                this.helpBox.show();
                this.screen.render();
            } else {
                this.helpBox.hide();
                this.screen.render();
            }
        });

        // Gère la fermeture et le retour
        this.screen.key(['escape', 'q'], () => {
            if (!this.helpBox.hidden) {
                this.helpBox.hide();
                this.tree.focus();
                this.screen.render();
            } else if (!this.searchInput.hidden) {
                this.searchInput.hide();
                this.tree.focus();
                this.screen.render();
            } else if (this.objectStack.length > 0) {
                const lastState = this.objectStack.pop();
                this.displayObject(lastState.obj);
                this.tree.rows.select(lastState.selectedNode);
                this.tree.focus();
                this.screen.render();
            } else {
                //this.destroy();
                process.exit(0);
            }
        });

        // Gère la recherche
        this.screen.key('/', () => {
            this.loggerBox.log('search');
            this.searchInput.show();
            this.searchInput.focus();
            this.screen.render();
        });

        this.searchInput.on('submit', (value) => {
            //this.loggerBox.log('search submit');
            this.searchKeyword = value;
            this.lastSearchIndex = -1;
            this.findNextNode(this.searchKeyword);
            this.searchInput.hide();
            this.screen.render();
        });

        this.searchInput.on('cancel', () => {
            //this.loggerBox.log('search cancel');
            this.searchInput.hide();
            this.tree.focus();
            this.screen.render();
        });

        this.screen.key('n', () => {
            //this.loggerBox.log('search next');
            if (this.searchKeyword) {
                this.findNextNode(this.searchKeyword);
            }
        });
    }

    // --- Logique d'affichage et de recherche ---

    displayObject(obj, context = '') {
        this.currentObject = obj;
        this.flatNodeList = this.flattenTreeData(obj);
        this.tree.setData({ name: (context?context:'root'), extended: true, children: this.buildTree(obj) });
        this.tree.focus();
        //this.updatePath();
    }

    updatePath() {
        this.loggerBox.log('updatePath');
        const selectedItem = this.tree.rows.items[this.tree.rows.selected];
        if (selectedItem) {
            const nodeName = selectedItem.content.trim();
            const selectedNode = this.findNodeByName(this.currentObject, nodeName);
            if (selectedNode) {
                //this.loggerBox.log('updatePath selectednode');
                const pathItems = this.getPathFromNode(selectedNode, this.currentObject);
                this.pathList.setItems(pathItems);
            }
        }
        this.screen.render();
    }

    findNextNode(keyword) {
        //this.loggerBox.log('findNextNode');
        let startIndex = this.lastSearchIndex + 1;
        let foundIndex = -1;
        const visibleItems = this.tree.rows.items;

        for (let i = startIndex; i < this.flatNodeList.length; i++) {
            const node = this.flatNodeList[i];
            if (node.name.toLowerCase().includes(keyword.toLowerCase())) {
                foundIndex = i;
                break;
            }
        }

        if (foundIndex === -1 && startIndex > 0) {
            for (let i = 0; i < startIndex; i++) {
                const node = this.flatNodeList[i];
                if (node.name.toLowerCase().includes(keyword.toLowerCase())) {
                    foundIndex = i;
                    break;
                }
            }
        }

        if (foundIndex !== -1) {
            this.lastSearchIndex = foundIndex;
            const targetNode = this.flatNodeList[foundIndex];
            const visibleIndex = visibleItems.findIndex(item => item.content.trim() === targetNode.name);
            if (visibleIndex !== -1) {
                this.tree.rows.select(visibleIndex);
                //this.updatePath();
            }
        } else {
            this.screen.log('Aucun résultat trouvé.');
            this.screen.render();
        }
    }

    // --- Fonctions utilitaires ---

    buildTree(data, labelPrefix = '') {
        const nodes = {};
        for (const key in data) {
            if (Object.prototype.hasOwnProperty.call(data, key)) {
                const value = data[key];
                const isObject = typeof value === 'object' && value !== null;
                const label = `${labelPrefix}${key}:`;

                if (isObject) {
                    nodes[label] = {
                        name: `${label}`,
                        extended: false,
                        children: this.buildTree(value, '  ' + labelPrefix)
                    };
                } else {
                    nodes[`${label} ${JSON.stringify(value)}`] = {
                        name: `${label} ${JSON.stringify(value)}`
                    };
                }
            }
        }
        return nodes;
    }

    flattenTreeData(node) {
        let nodes = [];
        if (node.name) {
            nodes.push(node);
        }
        if (node.children) {
            for (const childKey in node.children) {
                nodes = nodes.concat(this.flattenTreeData(node.children[childKey]));
            }
        }
        return nodes;
    }

    findNodeByName(root, name) {
        //this.loggerBox.log('findNodeByName');
        if (root.name === name) return root;
        if (root.children) {
            for (let childName in root.children) {
                const result = this.findNodeByName(root.children[childName], name);
                if (result) return result;
            }
        }
        return null;
    }

    getPathFromNode(node, root) {
        //this.loggerBox.log('getPathFromNode');
        let pathItems = [];
        let currentNode = node;
        while (currentNode && currentNode.name !== root.name) {
            pathItems.unshift(currentNode.name);
            let parentNode = null;
            function findParent(tree, childName) {
                if (tree.children) {
                    for (let key in tree.children) {
                        if (tree.children[key].name === childName) {
                            return tree;
                        }
                        let found = findParent(tree.children[key], childName);
                        if (found) return found;
                    }
                }
                return null;
            }
            currentNode = findParent(root, currentNode.name);
        }
        return ['/'].concat(pathItems);
    }
}