import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, OnInit, signal } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { IconButtonComponent, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { RulesService } from '../../../services/rules.service';
import { firstValueFrom } from 'rxjs';
import { DOSSIER_TEMPLATE_ID, DroolsKeywords, IRules } from '@red/domain';
import { EditorThemeService } from '@services/editor-theme.service';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import { Debounce, getParam } from '@iqser/common-ui/lib/utils';
import { ActivatedRoute } from '@angular/router';
import { rulesScreenTranslations } from '../../../translations/rules-screen-translations';
import ICodeEditor = monaco.editor.ICodeEditor;
import IModelDeltaDecoration = monaco.editor.IModelDeltaDecoration;
import IStandaloneEditorConstructionOptions = monaco.editor.IStandaloneEditorConstructionOptions;
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { MatIcon } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';
import { NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';

interface SyntaxError {
    line: number;
    column: number;
    message: string;
    warning?: boolean;
}

interface UploadResponse {
    blacklistErrorMessages: SyntaxError[];
    syntaxErrorMessages: SyntaxError[];
    deprecatedWarnings: SyntaxError[];
}

const RULE_VALIDATION_TIMEOUT = 2000;

@Component({
    templateUrl: './rules-screen.component.html',
    styleUrls: ['./rules-screen.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    standalone: true,
    imports: [MonacoEditorModule, MatIcon, FormsModule, IconButtonComponent, NgIf, TranslateModule],
})
export default class RulesScreenComponent implements OnInit, ComponentCanDeactivate {
    readonly translations = rulesScreenTranslations;

    readonly iconButtonTypes = IconButtonTypes;
    readonly editorOptions: IStandaloneEditorConstructionOptions = {
        theme: 'vs',
        language: 'java',
        automaticLayout: true,
        readOnly: !this.permissionsService.canEditRules(),
        glyphMargin: true,
        fixedOverflowWidgets: true,
    };
    initialLines: string[] = [];
    currentLines: string[] = [];
    isLeaving = false;
    readonly type: IRules['ruleFileType'];
    readonly #errorGlyphs = signal<string[]>([]);
    readonly numberOfErrors = computed(() => this.#errors().filter(e => !e.warning).length);
    readonly numberOfWarnings = computed(() => this.#errors().filter(e => e.warning).length);
    readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
    #codeEditor: ICodeEditor;
    #decorations: string[] = [];
    #errors = signal<SyntaxError[]>([]);
    #ruleValidationTimeout: number = null;

    set isLeavingPage(isLeaving: boolean) {
        this.isLeaving = isLeaving;
        this._changeDetectorRef.detectChanges();
    }

    get changed(): boolean {
        return this.currentLines.toString() !== this.initialLines.toString();
    }

    get codeEditorText() {
        return this.currentLines.join('\n');
    }

    set codeEditorText($event: any) {
        this.currentLines = $event.split('\n');
        this.codeEditorTextChanged();
        this.validateRules();
        this.#closeProblemsView();
    }

    constructor(
        readonly permissionsService: PermissionsService,
        private readonly _rulesService: RulesService,
        private readonly _changeDetectorRef: ChangeDetectorRef,
        private readonly _toaster: Toaster,
        private readonly _loadingService: LoadingService,
        private readonly _editorThemeService: EditorThemeService,
        private readonly _route: ActivatedRoute,
    ) {
        this.type = this._route.snapshot.data.type;
    }

    async ngOnInit() {
        await this.#initialize();
    }

    validateRules() {
        if (!this.changed) {
            this.#removeErrorMarkers();
        }
        if (this.#ruleValidationTimeout) {
            clearTimeout(this.#ruleValidationTimeout);
        }
        this.#ruleValidationTimeout = window.setTimeout(async () => {
            if (this.changed) {
                await this.#uploadRules(true);
            }
        }, RULE_VALIDATION_TIMEOUT);
    }

    async onCodeEditorInit(editor: ICodeEditor): Promise<void> {
        this.#codeEditor = editor;
        for (const theme of this._editorThemeService.themes) {
            (window as any).monaco.editor.defineTheme(theme, this._editorThemeService.configurations[theme]);
        }
        (window as any).monaco.editor.setTheme(this._editorThemeService.getTheme(true));
        await this.#configureSyntaxHighlighting();
        this._changeDetectorRef.detectChanges();
    }

    @Debounce()
    codeEditorTextChanged() {
        const newDecorations = this.currentLines.filter(entry => this.#isNew(entry)).map(entry => this.#makeDecorationFor(entry));
        this.#decorations = this.#codeEditor.deltaDecorations(this.#decorations, newDecorations);
    }

    goToErrors() {
        this.#codeEditor.trigger(null, 'editor.action.marker.next', null);
    }

    async save(): Promise<void> {
        this._loadingService.start();
        this.#removeErrorMarkers();
        clearTimeout(this.#ruleValidationTimeout);
        await this.#uploadRules();
    }

    async #uploadRules(dryRun = false) {
        return firstValueFrom(
            this._rulesService.uploadRules({
                rules: this.#getValue(),
                dossierTemplateId: this.#dossierTemplateId,
                ruleFileType: this.type,
                dryRun,
            }),
        ).then(
            async (response: UploadResponse) => {
                const errors = this.#mapErrors(response, dryRun);
                this.#drawErrorMarkers(errors);
                if (!dryRun) {
                    await this.#initialize();
                    this._toaster.success(rulesScreenTranslations[this.type]['success.generic']);
                }
            },
            error => {
                let errors: SyntaxError[];
                if (error.error?.syntaxErrorMessages) {
                    errors = this.#mapErrors(error.error, dryRun);
                } else {
                    const syntaxError: SyntaxError = { message: error.error.message, line: 1, column: 0 };
                    errors = this.#mapErrors(
                        { blacklistErrorMessages: [], syntaxErrorMessages: [syntaxError], deprecatedWarnings: [] },
                        dryRun,
                    );
                }
                this.#drawErrorMarkers(errors);
                this._loadingService.stop();
                if (!dryRun) {
                    this._toaster.error(rulesScreenTranslations[this.type]['error.generic']);
                }
            },
        );
    }

    revert(): void {
        this.currentLines = this.initialLines;
        this.#decorations = this.#codeEditor?.deltaDecorations(this.#decorations, []) || [];
        this.#removeErrorMarkers();
        this._changeDetectorRef.detectChanges();
        this._loadingService.stop();
    }

    #mapErrors(response: UploadResponse, dryRun = false) {
        const warnings = response.deprecatedWarnings.map(w => ({ ...w, warning: true }));
        if (dryRun) {
            return warnings;
        }
        return [...response.blacklistErrorMessages, ...response.syntaxErrorMessages, ...warnings];
    }

    #getValue(): string {
        this.#codeEditor.getModel().setEOL(monaco.editor.EndOfLineSequence.LF);
        return this.#codeEditor.getModel().getValue();
    }

    async #configureSyntaxHighlighting() {
        const languages = (window as any).monaco.languages.getLanguages();
        const javaLang = languages.find(l => l.id === 'java');
        const { language: config } = await javaLang.loader();
        DroolsKeywords.forEach(k => {
            if (!config.keywords.includes(k)) {
                config.keywords.push(k);
            }
        });

        // Update tokenizer to match keywords with hyphens (originally /[a-zA-Z_$][\w$]*/)
        config.tokenizer.root[1][0] = /[a-zA-Z_$][\w$-]*/;

        monaco.languages.registerCompletionItemProvider('java', {
            provideCompletionItems: (model, position) => {
                const word = model.getWordUntilPosition(position);
                const range = {
                    startLineNumber: position.lineNumber,
                    endLineNumber: position.lineNumber,
                    startColumn: word.startColumn,
                    endColumn: word.endColumn,
                };
                const suggestions = config.keywords.map(k => ({
                    label: k,
                    kind: monaco.languages.CompletionItemKind.Keyword,
                    insertText: k,
                    range,
                }));
                return { suggestions };
            },
        });
    }

    #isNew(entry: string): boolean {
        return this.initialLines.indexOf(entry) < 0 && entry?.trim().length > 0;
    }

    #makeDecorationFor(entry: string): IModelDeltaDecoration {
        const line = this.currentLines.indexOf(entry) + 1;

        return {
            range: new monaco.Range(line, 1, line, 1),
            options: { isWholeLine: true, className: 'changed-row-marker' },
        } as IModelDeltaDecoration;
    }

    #drawErrorMarkers(errors: SyntaxError[] | undefined) {
        const model = this.#codeEditor?.getModel();
        if (!model || !errors?.length) {
            this.#removeErrorMarkers();
            return;
        }
        const markers = [];
        const glyphs = [];
        errors
            .filter(e => e.line > 0)
            .forEach(e => {
                const endColumn = model.getLineLength(e.line) + 1;
                markers.push({
                    message: e.message,
                    severity: e.warning ? monaco.MarkerSeverity.Warning : monaco.MarkerSeverity.Error,
                    startLineNumber: e.line,
                    startColumn: e.column,
                    endLineNumber: e.line,
                    endColumn,
                });
                glyphs.push({
                    range: new monaco.Range(e.line, e.column, e.line, endColumn),
                    options: {
                        glyphMarginClassName: `glyph-margin ${e.warning ? 'warning' : 'error'}`,
                    },
                });
            });

        this.#errors.set(errors);
        this.#errorGlyphs.set(this.#codeEditor.deltaDecorations(this.#errorGlyphs(), glyphs));
        (window as any).monaco.editor.setModelMarkers(model, model.id, markers);
    }

    #closeProblemsView(): void {
        this.#codeEditor.trigger(null, 'closeMarkersNavigation', null);
    }

    #removeErrorMarkers(): void {
        const model = this.#codeEditor?.getModel();
        if (!model) {
            return;
        }
        (window as any).monaco.editor.setModelMarkers(model, model.id, []);
        this.#errors.set([]);
        this.#errorGlyphs.set(this.#codeEditor.deltaDecorations(this.#errorGlyphs(), []) || []);
        this.#closeProblemsView();
    }

    async #initialize() {
        this._loadingService.start();
        await firstValueFrom(this._rulesService.download(this.#dossierTemplateId, this.type)).then(
            rules => {
                this.currentLines = this.initialLines = rules.rules.split('\n');
                this.revert();
            },
            () => this._loadingService.stop(),
        );
    }
}
