import {
  DeesElement,
  property,
  html,
  customElement,
  type TemplateResult,
  css,
  cssManager,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { MONACO_VERSION } from './version.js';
import { themeDefaultStyles } from '../../00theme.js';

import type * as monaco from 'monaco-editor';

declare global {
  interface HTMLElementTagNameMap {
    'dees-workspace-monaco': DeesWorkspaceMonaco;
  }
}

@customElement('dees-workspace-monaco')
export class DeesWorkspaceMonaco extends DeesElement {
  // DEMO
  public static demo = () => html`<dees-workspace-monaco></dees-workspace-monaco>`;
  public static demoGroup = 'Workspace';

  // STATIC
  public static monacoDeferred: ReturnType<typeof domtools.plugins.smartpromise.defer>;

  // INSTANCE
  public editorDeferred = domtools.plugins.smartpromise.defer<monaco.editor.IStandaloneCodeEditor>();

  @property({
    type: String
  })
  accessor content = "function hello() {\n\talert('Hello world!');\n}";

  @property({
    type: String
  })
  accessor language = 'typescript';

  @property({
    type: String
  })
  accessor filePath: string = '';

  @property({
    type: Object
  })
  accessor contentSubject = new domtools.plugins.smartrx.rxjs.Subject<string>();

  @property({
    type: Boolean
  })
  accessor wordWrap: monaco.editor.IStandaloneEditorConstructionOptions['wordWrap'] = 'off';

  private monacoThemeSubscription: domtools.plugins.smartrx.rxjs.Subscription | null = null;
  private isUpdatingFromExternal: boolean = false;

  constructor() {
    super();
    domtools.DomTools.setupDomTools();
  }

  public static styles = [
    themeDefaultStyles,
    cssManager.defaultStyles,
    css`
      /* TODO: Migrate hardcoded values to --dees-* CSS variables */
      :host {
      }

      * {
        box-sizing: border-box;
      }

      #container {
        position: absolute;
        height: 100%;
        width: 100%;
      }
    `,
  ];

  public render(): TemplateResult {
    return html`
      <div class="mainbox">
        <div id="container"></div>
      </div>
    `;
  }

  public async firstUpdated(
    _changedProperties: Map<string | number | symbol, unknown>
  ): Promise<void> {
    super.firstUpdated(_changedProperties);
    const container = this.shadowRoot.getElementById('container');
    const monacoCdnBase = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}`;

    if (!DeesWorkspaceMonaco.monacoDeferred) {
      DeesWorkspaceMonaco.monacoDeferred = domtools.plugins.smartpromise.defer();
      const scriptUrl = `${monacoCdnBase}/min/vs/loader.js`;
      const script = document.createElement('script');
      script.src = scriptUrl;
      script.onload = () => {
        DeesWorkspaceMonaco.monacoDeferred.resolve();
      };
      document.head.appendChild(script);
    }
    await DeesWorkspaceMonaco.monacoDeferred.promise;

    (window as any).require.config({
      paths: { vs: `${monacoCdnBase}/min/vs` },
    });
    (window as any).require(['vs/editor/editor.main'], async () => {
      // Get current theme from domtools
      const domtoolsInstance = await this.domtoolsPromise;
      const isBright = domtoolsInstance.themeManager.goBrightBoolean;
      const initialTheme = isBright ? 'vs' : 'vs-dark';

      const monacoInstance = (window as any).monaco as typeof monaco;

      // Create or get model with proper file URI for TypeScript IntelliSense
      let model: monaco.editor.ITextModel | null = null;
      if (this.filePath) {
        const uri = monacoInstance.Uri.parse(`file://${this.filePath}`);
        model = monacoInstance.editor.getModel(uri);
        if (!model) {
          model = monacoInstance.editor.createModel(this.content, this.language, uri);
        } else {
          model.setValue(this.content);
        }
      }

      const editor = (monacoInstance.editor as typeof monaco.editor).create(container, {
        model: model || undefined,
        value: model ? undefined : this.content,
        language: model ? undefined : this.language,
        theme: initialTheme,
        useShadowDOM: true,
        fontSize: 16,
        automaticLayout: true,
        wordWrap: this.wordWrap,
        hover: {
          enabled: true,
          delay: 300,
          sticky: true,
          above: false,
        },
      });

      // Subscribe to theme changes
      this.monacoThemeSubscription = domtoolsInstance.themeManager.themeObservable.subscribe((goBright: boolean) => {
        const newTheme = goBright ? 'vs' : 'vs-dark';
        editor.updateOptions({ theme: newTheme });
      });

      this.editorDeferred.resolve(editor);
    });
    const css = await (
      await fetch(`${monacoCdnBase}/min/vs/editor/editor.main.css`)
    ).text();
    const styleElement = document.createElement('style');
    styleElement.textContent = css;
    this.shadowRoot.append(styleElement);


    // editor is setup let do the rest
    const editor = await this.editorDeferred.promise;
    editor.onDidChangeModelContent(async eventArg => {
      // Don't emit events when we're programmatically updating the content
      if (this.isUpdatingFromExternal) return;

      const value = editor.getValue();
      this.contentSubject.next(value);
      this.dispatchEvent(new CustomEvent('content-change', {
        detail: value,
        bubbles: true,
        composed: true,
      }));
    });
    this.contentSubject.next(editor.getValue());
  }

  public async updated(changedProperties: Map<string, any>): Promise<void> {
    super.updated(changedProperties);

    const monacoInstance = (window as any).monaco as typeof monaco;
    if (!monacoInstance) return;

    // Handle filePath changes - switch to different model
    if (changedProperties.has('filePath') && this.filePath) {
      const editor = await this.editorDeferred.promise;
      const uri = monacoInstance.Uri.parse(`file://${this.filePath}`);
      let model = monacoInstance.editor.getModel(uri);

      if (!model) {
        model = monacoInstance.editor.createModel(this.content, this.language, uri);
      } else {
        // Update model content if different
        if (model.getValue() !== this.content) {
          this.isUpdatingFromExternal = true;
          model.setValue(this.content);
          this.isUpdatingFromExternal = false;
        }
      }

      // Switch editor to use this model
      const currentModel = editor.getModel();
      if (currentModel?.uri.toString() !== uri.toString()) {
        editor.setModel(model);
      }
      return; // filePath change handles content too
    }

    // Handle content changes (when no filePath or filePath unchanged)
    if (changedProperties.has('content')) {
      const editor = await this.editorDeferred.promise;
      const currentValue = editor.getValue();
      if (currentValue !== this.content) {
        this.isUpdatingFromExternal = true;
        editor.setValue(this.content);
        this.isUpdatingFromExternal = false;
      }
    }

    // Handle language changes
    if (changedProperties.has('language')) {
      const editor = await this.editorDeferred.promise;
      const model = editor.getModel();
      if (model) {
        monacoInstance.editor.setModelLanguage(model, this.language);
      }
    }
  }

  public async disconnectedCallback(): Promise<void> {
    await super.disconnectedCallback();
    if (this.monacoThemeSubscription) {
      this.monacoThemeSubscription.unsubscribe();
      this.monacoThemeSubscription = null;
    }
  }

  /**
   * Update content from external source with optional cursor preservation.
   * Use this when the file content changes externally (e.g., file changed on disk).
   * @param newContent The new content to set
   * @param preserveCursor Whether to preserve cursor/scroll position (default: true)
   */
  public async setContentExternal(
    newContent: string,
    preserveCursor: boolean = true
  ): Promise<void> {
    const editor = await this.editorDeferred.promise;
    const currentValue = editor.getValue();

    if (currentValue === newContent) return;

    // Save cursor state if preserving
    const position = preserveCursor ? editor.getPosition() : null;
    const selections = preserveCursor ? editor.getSelections() : null;
    const scrollTop = preserveCursor ? editor.getScrollTop() : 0;
    const scrollLeft = preserveCursor ? editor.getScrollLeft() : 0;

    // Update content
    this.isUpdatingFromExternal = true;
    editor.setValue(newContent);
    this.isUpdatingFromExternal = false;

    // Restore cursor state if preserving
    if (preserveCursor) {
      if (position) {
        // Clamp position to valid range
        const model = editor.getModel();
        const lineCount = model?.getLineCount() || 1;
        const clampedLine = Math.min(position.lineNumber, lineCount);
        const lineLength = model?.getLineMaxColumn(clampedLine) || 1;
        const clampedColumn = Math.min(position.column, lineLength);
        editor.setPosition({ lineNumber: clampedLine, column: clampedColumn });
      }
      if (selections && selections.length > 0) {
        // Selections may be invalid after content change, wrap in try-catch
        try {
          editor.setSelections(selections);
        } catch {
          // Ignore invalid selections
        }
      }
      editor.setScrollPosition({ scrollTop, scrollLeft });
    }
  }
}
