/** This file is part of Reactor.
 *  Copyright (c) 2020-2025 Kedron Holdings LLC, All Rights Reserved.
 *  Reactor is not public domain or open source. Distribution or derivative works are expressly prohibited.
 */

export const version = 25311;

import api from '/client/ClientAPI.js';
import Reaction from '/client/Reaction.js';
import Container from '/client/Container.js';
import Data from '/client/Data.js';
import Rulesets from '/client/Rulesets.js';
import Observer from '/client/Observer.js';

import { isEmpty, idSelector, showSysModal, asyncForEach } from './reactor-ui-common.js';
import { mixin } from '/client/util.js';

import { _T } from './i18n.js';

import ReactionEditor from "./reaction-editor.js";

export default class ReactionList {
    constructor( $tab, $container, title ) {
        this.$tab = $tab;
        this.$container = $container;
        this.title = title || _T( ["#reactions-page-title","Global Reactions"] );

        this.editor = null;
        this.queue_data = null;

        this.$container.data( 'ReactionList', this );
    }

    async toggleDetail( event ) {
        const $row = $( event.currentTarget ).closest( 'div.re-reaction' );
        const id = $row.attr( 'id' );
        const $el = $( '#detail-' + id + '.collapse', $row );
        if ( ! $el.hasClass( 'loaded' ) ) {
            try {
                const reaction = await Reaction.getInstance( id );
                await reaction.refresh();
                const $body = $( '.card-body', $el ).empty();
                if ( 0 === (reaction.actions || []).length ) {
                    $body.text( _T('This reaction is empty.') );
                } else {
                    const $list = $( '<ol></ol>' ).appendTo( $body );
                    await asyncForEach( reaction.actions || [], async ( action ) => {
                        try {
                            let str = await ReactionEditor.makeActionDescription( action );
                            $( '<li></li>' ).text( str ).appendTo( $list );
                        } catch( e ) {
                            console.log( e );
                            $( '<li></li>' ).text( "ERROR" ).appendTo( $list );
                        }
                    });
                }
                const $rr = $( '<div></div>' )
                    .text( _T('Reaction ID: {0}', "" ) )
                    .appendTo( $body );
                $( '<span></span>' ).addClass( "user-select-all" )
                    .text( reaction.id )
                    .appendTo( $rr );
                $el.addClass( 'loaded' );
            } catch( err ) {
                return false;
            }
        }
        if ( $el.hasClass( 'show' ) ) {
            $el.slideUp( 250 ).removeClass( 'show' );
            $( '.col:first > i.bi', $row ).removeClass( 'bi-chevron-down' ).addClass( 'bi-chevron-right' );
        } else {
            $el.slideDown( 250 ).addClass( 'show' );
            $( '.col:first > i.bi', $row ).removeClass( 'bi-chevron-right' ).addClass( 'bi-chevron-down' );
        }
        return false;
    };

    editReaction( reaction, isNew ) {
        const self = this;
        this.$tab.empty();

        let newReaction = !!isNew;

        const $ct = $('<div class="container-fluid re-editor-controls"></div>' ).appendTo( this.$tab );
        const $row = $( '<div class="row"></div>' ).appendTo( $ct );
        $( '<div class="col"><h1></h1></div>' ).appendTo( $row );
        $( `<div class="col text-end mt-3">
  <button class="btn btn-sm btn-success saveconf">${_T(['#edit-button-save','Save'])}</button>
  <button class="btn btn-sm btn-danger revertconf">${_T(['#edit-button-exit','Exit'])}</button>
</div>` ).appendTo( $row );
        $( 'btn', $row ).prop( 'disabled', true );
        $( 'h1', $row ).text( reaction.name || reaction.id ).on( 'click', function( ev ) {
            const $el = $( ev.currentTarget );
            const $form = $( '<div class="form-group"><input id="newname" class="form-control form-control-sm"></div>' );
            $( 'input', $form ).val( $el.text() );
            showSysModal({
                title: _T(['#reaction-rename-title','Rename Reaction']),
                body: $form,
                buttons: [{
                    label: _T(['#reaction-rename-button-cancel','Cancel']),
                    class: "btn-primary",
                    close: true
                },{
                    label: _T(['#reaction-rename-button-rename','Rename']),
                    class: "btn-success",
                    event: "rename"
                }
            ]
            }).then( data => {
                if ( "rename" === data ) {
                    const newname = $( '#sysdialog #newname' ).val() || "";
                    if ( "" !== newname && newname !== reaction.name ) {
                        self.editor.rename( newname );
                        $el.text( newname );
                    }
                }
            });
            return false;
        });

        this.$tab.data( 'mode', 'edit' ).attr( 'data-mode', 'edit' );
        this.editor = new ReactionEditor( reaction, this.$tab, { fixedName: reaction.name || reaction.id } );
        this.editor.editor().on( 'update-controls', function( ev, ui ) {
            /* During editing, various changes make the reaction saveable, or not. */
            console.log("got update-controls");
            $( 'button.saveconf', self.$tab ).prop( 'disabled', ui.errors || !ui.modified );
            $( 'button.revertconf', self.$tab ).prop( 'disabled', false );
        }).on( 'close', function() {
            console.log(this, "got close event");
            self.editor = null;
            self.$tab.empty();
            self.$tab.data( 'mode', '' ).attr( 'data-mode', '' );
            self.$tab.trigger( 'activate' );
        }).on( 'modified', function( ev, ui ) {
            /* Sent when the cached reaction is modified (no errors) */
            console.log("got modified");
            $( 'button.saveconf', self.$tab ).prop( 'disabled', ui.errors || !ui.modified );
            $( 'button.revertconf', self.$tab ).prop( 'disabled', false );
        }).on( 'ready', event => {  // eslint-disable-line no-unused-vars
            return false; // VITAL for stopPropagation
        });

        $( 'button.saveconf', $ct ).on( 'click', async function( event ) {
            const $el = $( event.currentTarget );
            if ( ! self.editor.canSave() ) {
                showSysModal( {
                    title: 'Uncorrected errors remain!',
                    body: 'Please correct the indicated errors before saving.'
                } );
                return;
            }
            $( 'button.saveconf', self.$tab ).prop( 'disabled', true );
            const reaction = self.editor.data;
            console.log("Saving", reaction.id);
            const robj = await Reaction.getInstance( reaction.id );
            const dobj = robj.getDataObject();
            reaction.serial = ( reaction.serial || 0 ) + 1;
            reaction.mdt = Date.now();
            dobj.setValue( reaction );
            dobj.save().then( function() {
                self.editor.notifySaved();
                newReaction = false;
                if ( $el.hasClass( 'saveexit' ) ) {
                    self.closeEditor();
                }
            }).catch( function( err ) {
                console.log("save failed",err);
                showSysModal( {
                    title: _T('#dialog-error-title'),
                    body: _T('The save failed; try again. {0}')
                } );
                self.editor.updateSaveControls();
            });
            return false;
        });

        //console.log("Installing handler for exit button on", this);
        //console.error(new Error("trace"));
        $( 'button.revertconf', $ct ).on( 'click', async function( event ) {  // eslint-disable-line no-unused-vars
            //console.log(event);
            const reaction = self.editor.data;
            const robj = await Reaction.getInstance( reaction.id );
            if ( self.editor.isModified() ) {
                showSysModal( {
                    title: _T(['#reactionedit-unsaved-title','Reaction Modified (Unsaved Changes)']),
                    body: _T(['#reactionedit-unsaved-prompt','You have unsaved changes to this reaction. Really discard changes and exit?']),
                    buttons: [{
                        label: _T(['#reactionedit-unsaved-button-discard','Discard Changes']),
                        event: "discard",
                        class: "btn-danger"
                    },{
                        label: _T(['#reactionedit-unsaved-button-cancel','Cancel']),
                        close: true,
                        class: "btn-success"
                    }]
                }).then( async data => {
                    if ( "discard" === data ) {
                        if ( newReaction ) {
                            await robj.delete();
                        }
                        self.closeEditor();
                    }
                });
            } else {
                if ( newReaction ) {
                    await robj.delete();
                }
                self.closeEditor();
            }
            return false;
        });


        this.editor.edit();
        if ( newReaction ) {
            this.editor.signalModified();
        }
    }

    closeEditor() {
        if ( this.editor ) {
            this.editor.close();
            // this.editor = false;
        }
        // this.$tab.trigger( 'activate' );
    }

    setEnabledToggle( $btn, state ) {
        if ( state ) {
            $( 'i', $btn ).removeClass( 'bi-toggle-off' ).addClass( 'bi-toggle-on' );
            $btn.removeClass( 'text-danger' ).addClass( 'text-success' )
                .attr( 'title', _T("Enabled; click to disable") );
        } else {
            $( 'i', $btn ).removeClass( 'bi-toggle-on' ).addClass( 'bi-toggle-off' );
            $btn.removeClass( 'text-success' ).addClass( 'text-danger' )
                .attr( 'title', _T("Disabled; click to enable") );
        }
    }

    async handleReactionCopy( ev ) {
        const self = this;
        const $el = $( ev.currentTarget );
        const $row = $el.closest( '.re-reaction' );
        const id = $row.attr( 'id' );
        const src = await Reaction.getInstance( id );
        const rulesets = await Rulesets.getRulesets();
        const $mm = $( '<select id="rulesetlist" class="form-select form-select-sm ms-1"></select>' );
        if ( src.ruleset ) {
            $( '<option></option>' ).val( "" ).text( _T('(to current rule set)') ).appendTo( $mm );
        }
        $( '<option></option>' ).val( "$global$" ).text( _T( 'Global Reactions' ) ).appendTo( $mm );
        let $gr = $('<optgroup></optgroup>').attr( "label", _T("Rulesets") ).appendTo( $mm );
        rulesets.forEach( set => {
            if ( null === set ) { /* unable to fetch set in sequence */
                return;
            }
            if ( set.id === src.ruleset ) {
                return;
            }
            $( '<option></option>' ).val( set.id ).text( set.name || set.id )
                .appendTo( $gr );
        });
        $mm.on( 'change', (ev) => {
            const $mm = $( ev.currentTarget );
            $( 'div.modal-footer button[data-index="move"]' ).prop( 'disabled', "" === $mm.val() );
            return false;
        });
        const $div = $( '<form></form>' );
        $( `<label for="rulesetlist">${_T(['#reaction-move-target-label','Copy or move to:'])}</label>` )
            .appendTo( $div );
        $mm.appendTo( $div );
        showSysModal( {
            title: _T(['#reaction-move-title','Copy/Move Reaction']),
            body: $div,
            buttons: [{
                label: _T(['#reaction-move-button-move','Move Reaction']),
                event: "move",
                disabled: true,
                class: "btn-warning"
            },{
                label: _T(['#reaction-move-button-copy','Copy Reaction']),
                event: "copy",
                class: "btn-success"
            },{
                label: _T(['#reaction-move-button-cancel','Cancel']),
                close: true,
                class: "btn-primary"
            }]
        }).then( async event => {
            let rsid = $( '#sysdialog select#rulesetlist' ).val() || "";
            console.log("Modal",event,src.id,"from",src.ruleset,"to",rsid);
            if ( "move" === event ) {
                if ( isEmpty( rsid ) || rsid === src.ruleset || ( "$global$" === rsid && !src.ruleset ) ) {
                    /* Already in target ruleset */
                    return event;
                }
                await src.move( "$global$" === rsid ? undefined : rsid );
                $row.remove();
            } else if ( "copy" === event ) {
                /* ??? FIXME -- should not be working on dobjs directly */
                Reaction.create().then( dest => {
                    const dobj = dest.getDataObject();
                    dobj.value.name = src.name + ' Copy';
                    dobj.value.serial = 1;
                    dobj.value.actions = [ ...src.actions ];
                    dobj.forceModified().save().then( () => {
                        /* Copy to current; means we need to show here, and move to src's ruleset */
                        if ( isEmpty( rsid ) ) {
                            rsid = src.ruleset;
                            self.getReactionRow( dest ).insertAfter( $row );
                            self.subscribe( dest, self.handleReactionEvent.bind( self ) );
                        }
                        /* Created as global, so if target is other than global, move it */
                        if ( "$global$" !== rsid ) {
                            dest.move( rsid );
                        }
                    });
                });
            } else {
                /* Cancel */
            }
        });
        return false;
    }

    async updateReactionRow( reaction, $row ) {
        $row = $row || $( `div.re-reaction#${idSelector( reaction.id )}` );
        // console.log("Row for",reaction.id,"is",$row);

        /*
        const dobj = await Data.getInstance( Container.getInstance( 'states' ), 'reaction_history' );
        if ( dobj && Array.isArray( dobj.value?.history ) ) {
            let el = dobj.value.history.findLast( el => el.reaction_id === reaction.id );
            if ( el ) {
                $( 'div.re-reaction-lastrun', $row ).text( new Date( el.start_time ).toLocaleString() );
            }
        }
        */

        $( 'span.re-reaction-name', $row ).text( reaction.name || reaction.id )
            .attr( 'title', String( reaction.name ) + " ID " + reaction.id );

        const $el = $( 'button[data-action="disable"]', $row );
        this.setEnabledToggle( $el, !reaction.disabled );
    };

    async handleReactionEvent( event ) {
        //console.log( 'handleReactionEvent', event );
        if ( 'reaction-changed' === event.type ) {
            this.updateReactionRow( event.data );
            this.handleReactionListFilterChange();
        } else if ( 'reaction-deleted' === event.type ) {
            const $row = $( `div.re-reaction#${idSelector( event.data.id )}` );
            $row.remove();
        }
    };

    getReactionRow( reaction ) {
        const $row = $( '<div class="row re-listport-item re-reaction"></div>' ).attr( 'id', reaction.id );
        let $c = $( '<div class="col pt-1 text-nowrap"></div>' )
            .appendTo( $row );
        $( `<button class="btn bi-btn bi-btn-white me-2 text-success" title="${_T(['#reaction-action-run','Run Now'])}" data-action="run"><i class="bi bi-play"></i></button>` )
            .appendTo( $c );
        $( '<span class="re-reaction-name"></span>' ).text( reaction.name || reaction.id )
            .on( 'click', this.toggleDetail.bind( this ) )
            .appendTo( $c );
        $( '<i class="bi bi-chevron-right ms-1"></i>' )
            .on( 'click', this.toggleDetail.bind( this ) )
            .appendTo( $c );

        /* See also updateReactionRow()
        $c = $( '<div class="col text-end re-reaction-lastrun"></div>' )
            .text( "-" )
            .appendTo( $row );
        */

        $c = $( '<div class="col text-end re-reaction-controls"></div>' )
            .appendTo( $row );
        $( '<button class="btn bi-btn bi-btn-white ms-2 text-success" data-action="disable" title="Enable/Disable Reaction"><i class="bi bi-toggle-on"></i></button>' )
            .appendTo( $c );
        $( `<button class="btn bi-btn bi-btn-white ms-2 text-primary" data-action="edit" title="${_T(['#reaction-action-edit','Edit this Reaction'])}"><i class="bi bi-pencil"></i></button>` )
            .appendTo( $c );
        $( `<button class="btn bi-btn bi-btn-white ms-2 text-secondary" data-action="copy" title="${_T(['#reaction-action-copy','Copy Reaction'])}"><i class="bi bi-share-fill"></i></button>` )
            .appendTo( $c );
        $( `<button class="btn bi-btn bi-btn-white ms-4 text-danger" data-action="delete" title="${_T(['#reaction-action-delete','Delete this Reaction'])}"><i class="bi bi-x"></i></button>` )
            .appendTo( $c );

        /* Detail card (collapsible) */
        $( '<div class="w-100"></div>').appendTo( $row );
        $( '<div class="col-xs-12 col-sm-12 collapse"><div class="card card-body">Jeffrey Epstein didn\'t kill himself.</div></div>' )
            .attr( 'id', 'detail-' + reaction.id )
            .appendTo( $row );

        $( 'button[data-action="run"]', $row ).on( 'click', async ( event ) => {
            const $el = $( event.currentTarget );
            const $row = $el.closest( '.re-reaction' );
            const rid = $row.attr( 'id' );
            const reaction = await Reaction.getInstance( rid );
            console.log("Reaction launching",rid);
            reaction.launch();
            return false;
        });

        const self = this;

        $( 'button[data-action="disable"]', $row ).on( 'click', async function( event ) {
            const $el = $( event.currentTarget );
            const rid = $el.closest( '.re-reaction' ).attr( 'id' );
            const robj = await Reaction.getInstance( rid );
            /* ??? FIXME -- should update based on change notification for underlying object */
            self.setEnabledToggle( $el, robj.disabled );
            robj.enable( robj.disabled );
            return false;
        });

        $( 'button[data-action="copy"]', $row ).on( 'click', this.handleReactionCopy.bind( this ) );

        $( 'button[data-action="delete"]', $row ).on( 'click', async function( event ) {
            const $el = $( event.currentTarget );
            const $row = $el.closest( '.re-reaction' );
            const rid = $row.attr( 'id' );
            const robj = await Reaction.getInstance( rid );
            showSysModal( {
                title: _T(['#reaction-delete-title','Confirm Deletion']),
                body: _T(['#reaction-delete-prompt','Really delete {0:q}? There is no "undo"'], robj.name),
                buttons: [
                    {
                        label: _T(['#reaction-delete-button-delete','Delete']),
                        class: "btn-danger",
                        event: "delete"
                    },
                    {
                        label: _T(['#reaction-delete-button-cancel','Cancel']),
                        class: "btn-success"
                    }
                ]
            } ).then( async function( data ) {
                if ( "delete" === data ) {
                    robj.delete().then( () => {
                        /* Remove the displayed row */
                        $row.remove();
                    });
                }
                return data;
            } );
            return false;
        } );

        //console.log($row,"configuring edit button");
        $( 'button[data-action="edit"]', $row ).on( 'click', async function( ev ) {
            console.log("edit button handler",ev);
            const rid = $( ev.target ).closest( 'div.re-reaction' ).attr( 'id' );
            const reaction = await Reaction.getInstance( rid );
            if ( ! reaction ) {
                console.log("Reaction " + String(rid) + " not found");
                return;
            }
            await reaction.refresh();  // make sure we're looking at latest and greatest
            self.editReaction( reaction.getDataObject().value );
            return false;
        });

        return $row;
    };

    async handleReactionListFilterChange( /* event */ ) {
        const $field = $( '.re-listport-head input#filter-name' );
        const prefix = ( $field.val() || "" ).toLowerCase();
        $( '.re-listport-body .re-reaction' ).each( (ix,el) => {
            const $el = $( el );
            const reName = $( 'span.re-reaction-name', $el ).text().toLowerCase();
            $el.toggle( reName.indexOf( prefix ) >= 0 );
        });
        localStorage.setItem( 'reactionlist_filter_name', prefix );

        return false;
    };

    async show( reactions, ruleset ) {
        console.log("Reaction list show", reactions);
        const self = this;

        this.$container.empty();
        const $ct = $( '<div class="re-listport container-fluid ps-0"></div>' ).appendTo( this.$container );

        const $section = $( `<div class="re-listport-head container-fluid ps-0">
  <div class="row align-middle">
    <div class="col-auto align-top text-nowrap">
      <span class="re-listport-title">${this.title}</span>
    </div>
    <div class="col-auto align-top mt-1 text-nowrap">
      <input type="search" id="filter-name" class="form-control form-control-sm reaction-list-filter" autocomplete="off">
    </div>
    <div class="col text-end mt-1">
      <button id="newreaction" class="btn btn-sm btn-success">${_T(['#reactions-button-create','Create Reaction'])}</button>
    </div>
  </div>
</div>` )
            .appendTo( $ct );

        const handler = this.handleReactionListFilterChange.bind( this );
        let $e = $( 'input.reaction-list-filter', $section );
        $e.val( localStorage.getItem( 'reactionlist_filter_name' ) || "" )
            .attr( 'placeholder', _T( 'Name Contains:' ) )
            .on( 'keyup', handler )
            .on( 'change', handler )
            .on( 'search', handler );

        const $body = $( '<div class="re-listport-body container-fluid ps-0"></div>' )
            .appendTo( $ct );

        reactions.sort( function( a, b ) {
            return (a.name || a.id).localeCompare( b.name || b.id, undefined, { sensitivity: 'base' } );
        });
        const reactionEventHandler = this.handleReactionEvent.bind( this );
        for ( let reaction of reactions ) {
            /* ??? better to use ul/li ? */
            const $row = this.getReactionRow( reaction );
            $row.appendTo( $body );
            this.updateReactionRow( reaction, $row );
            this.subscribe( reaction, reactionEventHandler );
        }

        $( 'button#newreaction' ).on( 'click', async () => {
            const reaction = await Reaction.create();
            if ( ruleset ) {
                await reaction.move( ruleset.getID() );
            }
            self.editReaction( reaction.getDataObject().value, true );
            return false;
        });

        this.handleReactionListFilterChange( {} );

        const dobj = await Data.getInstance( Container.getInstance( 'states' ), 'reaction_queue' );
        if ( dobj ) {
            this.queue_data = dobj;
            this.subscribe( this.queue_data, ( ev ) => {
                //console.log("reaction queue change", dobj.value);
                self.updateRunningIndicators( self.queue_data );
            });
            this.updateRunningIndicators( this.queue_data );
        }

        api.off( "structure_update.reaction_list" ).on( "structure_update.reaction_list", () => {
            console.log( "reaction-list: refreshing after structure update" );
            $( 'div.re-reaction', self.$container ).each( (ix, obj) => {
                const $row = $( obj );
                Reaction.getInstance( $row.attr( 'id' ) || "" ).then( reaction => {
                    self.updateReactionRow( reaction, $row );
                }).catch( err => {
                    console.error( "reaction-list: can't fetch", $row.attr( 'id' ), ":", err );
                });
            });
            self.updateRunningIndicators( self.queue_data );
        });
        return false;
    };

    updateRunningIndicators( dobj ) {
        // Get list of reactions previously running.
        const olds = new Set();
        $( 'span.re-reaction-running', this.$tab ).closest( 'div.re-reaction' ).each( (ix,el) => {
            olds.add( $( el ).attr( 'id' ) );
        });
        for ( let entry of dobj.value ) {
            if ( olds.has( entry.id ) ) {
                olds.delete( entry.id );  // still running
            } else {
                const $row = $( `div.re-reaction#${idSelector(entry.id)}`, this.$tab );
                const $p = $( '<span class="re-reaction-running ms-2 spinner-border spinner-border-sm text-success" role="status"></span>' );
                $( `span.re-reaction-name`, $row ).parent().append($p);
                $row.stop().clearQueue().css( 'background-color', 'var(--bs-green)' );
                $row.animate( { backgroundColor: 'transparent' }, 2000);
            }
        }
        // Remove spinners for reactions no longer running.
        for ( let id of olds.values() ) {
            const $row = $( `div.re-reaction#${idSelector(id)}`, this.$tab );
            $( 'span.re-reaction-running', $row ).remove();
            $row.stop().clearQueue().css( 'background-color', 'var(--bs-green)' );
            $row.animate( { backgroundColor: 'transparent' }, 2000);
        }
    }

    // Override method for reaction editor
    canSuspend() {
        if ( this.editor && this.editor.isModified() ) {
            console.log( "Stopping event in edit mode" );
            return false;
        }
        return true;
    }

    // Override method for reaction editor
    suspending() {
        console.log( "reaction-list", this, "suspending" );
        if ( this.editor ) {
            this.closeEditor();
        }

        api.off( ".reaction_list" );
        this.unsubscribe();

        if ( this.$container ) {
            this.$container.removeData( 'ReactionList' );
            this.$container.empty();
            this.$container = null;
        }
        this.$tab = null;

        delete this.editor;
        delete this.queue_data;
    }
};

mixin( ReactionList, Observer );
