import type {
    App,
    CachedMetadata,
    Debouncer,
    EditorPosition,
    EventRef,
    MarkdownPostProcessorContext,
    PluginManifest
} from "obsidian";
import { TFile, debounce } from "obsidian";
import { PluginBase } from "obsidian-dev-utils/obsidian/plugin/plugin-base";
import { initPluginContext } from "obsidian-dev-utils/obsidian/plugin/plugin-context";
import { ManageToc } from "./ManageToc";
import { deepMerge } from "./Utils";
import { instaTocCodeBlockId } from "./constants";
import EditorService from "./editorService";
import { PluginSettingsManager } from "./settings/PluginSettingManager";
import type { InstaTocSettings } from "./settings/Settings";
import { getDefaultLocalSettings } from "./settings/Settings";
import { SettingTab } from "./settings/SettingsTab";
import { CodeBlockComponent, LocalSettingsModal, MarkdownComponentMounter } from "./svelte";
import { TocModel } from "./tocModel";
import type { FileKey, LocalTocSettings, PluginTypes } from "./types";
import UiStateManager from "./uiStateManager";
import { Validator, hasInstaTocSection } from "./validator";

type ReloadedTocState = { activeFile: TFile; validator: Validator; isValid: boolean; };

type ReloadOpts = { forceValidate?: boolean; cache?: CachedMetadata; manageToc?: boolean; };

export default class InstaTocPlugin extends PluginBase<PluginTypes> {
    private _validator: Validator | undefined;
    private _editorService: EditorService | undefined;
    private _uiStateManager: UiStateManager | undefined;
    private modifyEventRef: EventRef | undefined;
    private debouncer!: Debouncer<[fileCache: CachedMetadata], void>;

    constructor(app: App, manifest: PluginManifest) {
        super(app, manifest);
    }

    public override get settings(): InstaTocSettings {
        return this.settingsManager.settingsWrapper.settings as InstaTocSettings;
    }

    public get validator(): Validator {
        assert(this._validator, "Validator is not initialized yet.");
        return this._validator;
    }

    public get editorService(): EditorService {
        assert(this._editorService, "EditorService is not initialied yet.");
        return this._editorService;
    }

    public get uiStateManager(): UiStateManager {
        assert(this._uiStateManager, "UiStateManager is not initialied yet.");
        return this._uiStateManager;
    }

    public get isMobile(): boolean {
        return this.app.isMobile;
    }

    protected override createSettingsManager(): PluginSettingsManager {
        const settingsManager = new PluginSettingsManager(this);
        this._uiStateManager ??= new UiStateManager(settingsManager);

        return settingsManager;
    }

    protected override createSettingsTab(): SettingTab {
        return new SettingTab(this);
    }

    public override async onloadImpl(): Promise<void> {
        await super.onloadImpl();
        initPluginContext(this.app, "insta-toc");
        console.log(`Loading Insta TOC Plugin`);

        this._uiStateManager ??= new UiStateManager(this.settingsManager);
        this._editorService = new EditorService(this.app, this.registerEvent.bind(this), [ {
            name: "file-open",
            // Immediate TOC update on file-open ensures TOC renders/updates for every file
            callback: (file) => {
                void this.uiStateManager.flushPersistedData();

                if (!(file instanceof TFile)) return;

                const cache = this._editorService?.cache;
                if (!cache) return;

                this.debouncer.cancel();
                this.debouncer(cache).run();
            }
        }, {
            name: "active-leaf-change",
            callback: () => {
                void this.uiStateManager.flushPersistedData();
            }
        }, {
            name: "layout-change",
            callback: () => {
                void this.uiStateManager.flushPersistedData();
            }
        }, {
            name: "rename",
            // Replace old fold state data upon rename
            callback: (file, oldPath) => {
                const newPath = file.path as FileKey;
                this.uiStateManager.pruneTocFoldStateForPath(oldPath, { replacementFile: newPath });
            }
        } ]);

        this.updateModifyEventListener();

        // Custom codeblock processor for the insta-toc codeblock
        this.registerMarkdownCodeBlockProcessor(
            "insta-toc",
            async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext): Promise<void> => {
                const sourcePath: FileKey = ctx.sourcePath as FileKey;
                const tocModel = new TocModel(this.uiStateManager, this.app, this.settings, {
                    localSettings: source,
                    sourceFilePath: sourcePath
                }, (key) => this.uiStateManager.getTocFoldState(key));

                const mounter = new MarkdownComponentMounter(el, {
                    component: CodeBlockComponent,
                    props: {
                        plugin: this,
                        sourcePath,
                        model: tocModel.model,
                        onOpenEditBlock: el.parentElement
                            ? () => {
                                el.parentElement!
                                    .querySelector<HTMLDivElement>(
                                        ".edit-block-button[aria-label='Edit this block']:not(.insta-toc-action-button)"
                                    )
                                    ?.click();
                            }
                            : null
                    }
                });

                ctx.addChild(mounter);
            }
        );

        this.addCommand({
            id: "add-insta-toc-block",
            name: "Add Insta TOC Block",
            allowPreview: true,
            editorCallback: async (_editor, _ctx) => {
                await this.addInstaTocBlock();
            }
        });

        this.addRibbonIcon("table-of-contents", "Add Insta TOC Block", async (_evt) => {
            await this.addInstaTocBlock();
        });
    }

    protected override async onunloadImpl(): Promise<void> {
        this.debouncer?.run(); // flush pending TOC update
        await this.uiStateManager.flushPersistedData();
        await this.settingsManager.saveToFile();
        await super.onunloadImpl();

        console.log(`Insta TOC Plugin Unloaded.`);
        this.app.vault.configDir;
    }

    public async reload(
        { forceValidate = false, cache: cacheOverride, manageToc = true }: ReloadOpts = {}
    ): Promise<ReloadedTocState | undefined> {
        this.editorService.syncState(cacheOverride);
        const { editor, cache, file } = this.editorService;

        if (!editor || !cache || !file) {
            console.warn(`[WARNING] Unable to reload insta-toc.\neditor: ${editor}\ncache: ${cache}\nfile: ${file}`);
            return;
        }

        const sourcePath = file.path;

        if (this._validator) {
            this._validator.update(this.editorService, this.settings, cache, sourcePath);
        }
        else {
            this._validator = new Validator(this.editorService, this.settings, cache, sourcePath);
        }

        const validator = this.validator;
        const isValid = validator.isValid(forceValidate);

        if (manageToc && isValid) {
            await ManageToc.run(this.editorService, this.settings, validator);
        }

        return { activeFile: file, validator, isValid };
    }

    private async addInstaTocBlock(): Promise<void> {
        const service = this.editorService;
        service.syncState();
        const { file, cache, view, editor } = service;
        const hasSection: boolean | undefined = editor && cache?.sections
            ? hasInstaTocSection(editor, cache.sections)
            : undefined;
        if (!file || !cache || !view || hasSection === true) {
            const consoleMessage = hasSection !== true
                ? !file ? "[WARN] No active file to insert TOC into." : !cache
                    ? "[WARN] No metadataCache available."
                    // !view
                    : "[WARN] Active view is not a Markdown view."
                : "[WARN] InstaToc section detected in active file. Aborting...";
            new Notice(consoleMessage);
            console.log(consoleMessage);
            return;
        }
        this.app.plugins.getPlugin("");

        const fmEnd = cache.frontmatterPosition?.end;
        const insertPos: EditorPosition = fmEnd ? { line: fmEnd.line + 1, ch: 0 } : { line: 0, ch: 0 };
        const tocBlock = `\`\`\`${instaTocCodeBlockId}\n\n\`\`\`\n`;
        const tocString = fmEnd ? `\n\n${tocBlock}\n` : tocBlock;

        await service.applyEditorChange(insertPos, insertPos, tocString);
    }

    // Dynamically update the debounce delay for ToC updates
    public updateModifyEventListener(): void {
        if (this.modifyEventRef) {
            // Unregister the previous event listener
            this.app.metadataCache.offref(this.modifyEventRef);
        }

        this.setDebouncer();

        // Register the new event listener with the updated debounce delay
        this.modifyEventRef = this.app.metadataCache.on(
            "changed", // file cache (containing heading cache) has been updated
            (file: TFile, _data: string, cache: CachedMetadata) => {
                const activeFile: TFile | null = this.editorService.file;

                if (!activeFile || activeFile.path !== file.path) return;

                this.debouncer(cache);
            }
        );

        this.registerEvent(this.modifyEventRef);
    }

    // Needed for dynamically setting the debounce delay
    private setDebouncer(): void {
        this.debouncer = debounce(
            async (fileCache: CachedMetadata) => {
                const state = await this.reload({ cache: fileCache });

                if (!state) {
                    console.log("[WARNING] Unable to reload the active TOC state during the debounced update.");
                    return;
                }
            },
            this.settings.updateDelay,
            true
        );
    }

    public async openLocalSettingsModal(): Promise<void> {
        const state = await this.reload({ forceValidate: true, manageToc: false });

        assert(state, "TOC state is required before opening the local settings modal.");

        const initialSettings = state.isValid ? state.validator.localTocSettings : getDefaultLocalSettings();
        const mergedInitialSettings = deepMerge<LocalTocSettings>(getDefaultLocalSettings(), initialSettings);

        state.validator.localTocSettings = mergedInitialSettings;
        state.validator.updatedLocalSettings = mergedInitialSettings;

        await new LocalSettingsModal(this, async (result: string): Promise<boolean> => {
            const didApply = this.validator.applyLocalSettingsYaml(result);

            if (!didApply) return false;

            await ManageToc.run(this.editorService, this.settings, this.validator);

            return true;
        })
            .open();
    }
}
