// Copyright (C) 2025 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import m from 'mithril';
import {Engine} from 'syntaqlite';
import {assetSrc} from '../../base/assets';
import {Icons} from '../../base/semantic_icons';
import type {QueryResponse} from '../../components/query_table/queries';
import {QueryHistoryComponent} from '../../components/widgets/query_history';
import type {Trace} from '../../public/trace';
import {Box} from '../../widgets/box';
import {Button, ButtonVariant} from '../../widgets/button';
import {Callout} from '../../widgets/callout';
import {Intent} from '../../widgets/common';
import {Editor} from '../../widgets/editor';
import {EmptyState} from '../../widgets/empty_state';
import {HotkeyGlyphs} from '../../widgets/hotkey_glyphs';
import {Spinner} from '../../widgets/spinner';
import {SplitPanel} from '../../widgets/split_panel';
import {Tabs, type TabsTab} from '../../widgets/tabs';
import {Stack, StackAuto} from '../../widgets/stack';
import {Anchor} from '../../widgets/anchor';
import SqlModulesPlugin from '../dev.perfetto.SqlModules';
import {TableList} from './table_list';
import {ResultsTable} from './results_table';

const HIDE_PERFETTO_SQL_AGENT_BANNER_KEY = 'hidePerfettoSqlAgentBanner';

// Represents a single query editor tab with its own state.
export interface QueryEditorTab {
  readonly id: string;
  editorText: string;
  queryId?: number; // Unique ID associated with the current query result (session unique only, not universally unique).
  queryResult?: QueryResponse;
  isLoading: boolean;
  title: string;
}

export interface QueryPageAttrs {
  // The trace to run queries against.
  readonly trace: Trace;

  // All editor tabs.
  readonly editorTabs: QueryEditorTab[];

  // The currently active editor tab ID.
  readonly activeTabId: string;

  // Called when the content of an editor is updated.
  onEditorContentUpdate?(tabId: string, content: string): void;

  // Called when the user requests to execute a query.
  onExecute?(tabId: string, query: string): void;

  // Called when the user switches to a different tab.
  onTabChange?(tabId: string): void;

  // Called when the user closes a tab.
  onTabClose?(tabId: string): void;

  // Called when the user wants to add a new tab.
  onTabAdd?(
    tabName?: string,
    initialQuery?: string,
    autoExecute?: boolean,
  ): void;

  // Called when the user renames a tab.
  onTabRename?(tabId: string, newName: string): void;

  // Called when the user reorders tabs via drag and drop.
  // draggedTabId is the tab being moved, beforeTabId is the tab it should be
  // placed before (or undefined if moved to the end).
  onTabReorder?(draggedTabId: string, beforeTabId: string | undefined): void;
}

export class QueryPage implements m.ClassComponent<QueryPageAttrs> {
  // Lazily-initialized SQL formatter engine, scoped to this component instance.
  private formatterEnginePromise?: Promise<Engine>;

  private getFormatterEngine(): Promise<Engine> {
    if (this.formatterEnginePromise === undefined) {
      const engine = new Engine({
        runtimeJsPath: assetSrc('assets/syntaqlite-runtime.js'),
        runtimeWasmPath: assetSrc('assets/syntaqlite-runtime.wasm'),
      });
      this.formatterEnginePromise = (async () => {
        await engine.load();
        const binding = await engine.loadDialectFromUrl(
          assetSrc('assets/syntaqlite-perfetto.wasm'),
          'syntaqlite_perfetto_dialect_template',
        );
        engine.setDialectPointer(binding.ptr);
        return engine;
      })();
    }
    return this.formatterEnginePromise;
  }

  view({attrs}: m.CVnode<QueryPageAttrs>) {
    const {editorTabs, activeTabId} = attrs;

    // Build editor tabs for the left panel
    const leftTabs: TabsTab[] = editorTabs.map((tab) => ({
      key: tab.id,
      title: tab.title,
      leftIcon: 'code',
      closeButton: editorTabs.length > 1,
      content: this.renderEditorTabContent(attrs, tab),
    }));

    const leftPanel = m(Tabs, {
      className: 'pf-query-page__editor-tabs',
      tabs: leftTabs,
      activeTabKey: activeTabId,
      reorderable: true,
      onTabChange: (key) => attrs.onTabChange?.(key),
      onTabRename: (key, newTitle) => {
        attrs.onTabRename?.(key, newTitle);
      },
      onTabClose: (key) => attrs.onTabClose?.(key),
      onTabReorder: (draggedKey, beforeKey) =>
        attrs.onTabReorder?.(draggedKey, beforeKey),
      onNewTab: () => attrs.onTabAdd?.(),
    });

    const activeTab = editorTabs.find((t) => t.id === activeTabId);

    const sidebarPanel = m(Tabs, {
      className: 'pf-query-page__sidebar',
      tabs: [
        {
          key: 'history',
          title: 'History',
          leftIcon: 'history',
          content: m(QueryHistoryComponent, {
            className: 'pf-query-page__history',
            trace: attrs.trace,
            runQuery: (query: string) => {
              if (activeTab) {
                attrs.onExecute?.(activeTab.id, query);
              }
            },
            setQuery: (query: string) => {
              if (activeTab) {
                attrs.onEditorContentUpdate?.(activeTab.id, query);
              }
            },
          }),
        },
        {
          key: 'tables',
          title: 'Tables',
          leftIcon: 'table_chart',
          content: this.renderTablesTab(attrs),
        },
      ],
    });

    return m(
      '.pf-query-page',
      m(SplitPanel, {
        direction: 'horizontal',
        initialSplit: {pixels: 500},
        controlledPanel: 'second',
        minSize: 100,
        firstPanel: leftPanel,
        secondPanel: sidebarPanel,
      }),
    );
  }

  private renderEditorTabContent(
    attrs: QueryPageAttrs,
    tab: QueryEditorTab,
  ): m.Children {
    const {trace} = attrs;

    const editorPanel = m('.pf-query-page__editor-panel', [
      m(Box, {className: 'pf-query-page__toolbar'}, [
        m(Stack, {orientation: 'horizontal'}, [
          m(Button, {
            label: 'Run Query',
            icon: 'play_arrow',
            loading: tab.isLoading,
            intent: tab.isLoading ? Intent.None : Intent.Primary,
            variant: ButtonVariant.Filled,
            onclick: () => {
              attrs.onExecute?.(tab.id, tab.editorText);
            },
          }),
          m(
            Stack,
            {
              orientation: 'horizontal',
              className: 'pf-query-page__hotkeys',
            },
            'or press',
            m(HotkeyGlyphs, {hotkey: 'Mod+Enter'}),
          ),
          m(StackAuto), // The spacer pushes the following buttons to the right.
          m(Button, {
            label: 'Format',
            icon: 'format_align_left',
            title: 'Auto-format the SQL query',
            onclick: () => this.formatSql(attrs, tab.id, tab.editorText),
          }),
          trace.isInternalUser &&
            m(Button, {
              icon: 'wand_stars',
              title:
                'Generate SQL queries with the Perfetto SQL Agent! Give feedback: go/perfetto-llm-bug',
              label: 'Generate SQL Queries with AI',
              onclick: () => {
                window.open('http://go/perfetto-sql-agent', '_blank');
              },
            }),
        ]),
      ]),
      this.shouldDisplayPerfettoSqlAgentBanner(attrs) &&
        m(
          Box,
          m(
            Callout,
            {
              icon: 'wand_stars',
              dismissible: true,
              onDismiss: () => {
                this.hidePerfettoSqlAgentBanner();
              },
            },
            [
              'Try out the ',
              m(
                Anchor,
                {
                  href: 'http://go/perfetto-sql-agent',
                  target: '_blank',
                  icon: Icons.ExternalLink,
                },
                'Perfetto SQL Agent',
              ),
              ' to generate SQL queries and ',
              m(
                Anchor,
                {
                  href: 'http://go/perfetto-llm-user-guide#report-issues',
                  target: '_blank',
                  icon: Icons.ExternalLink,
                },
                'give feedback',
              ),
              '!',
            ],
          ),
        ),
      tab.editorText.includes('"') &&
        m(
          Box,
          m(
            Callout,
            {icon: 'warning', intent: Intent.None},
            `" (double quote) character observed in query; if this is being used to ` +
              `define a string, please use ' (single quote) instead. Using double quotes ` +
              `can cause subtle problems which are very hard to debug.`,
          ),
        ),
      m(Editor, {
        language: 'perfetto-sql',
        text: tab.editorText,
        onUpdate: (content) => attrs.onEditorContentUpdate?.(tab.id, content),
        onExecute: (query) => attrs.onExecute?.(tab.id, query),
        onFormat: (text) => this.formatSql(attrs, tab.id, text),
      }),
    ]);

    const resp = tab.queryResult;
    const resultsPanel = resp
      ? m(ResultsTable, {
          key: tab.queryId, // Force remount when query changes to reset internal state
          data: resp.error
            ? {kind: 'error', errorMessage: resp.error}
            : {
                kind: 'success',
                columns: resp.columns,
                rows: resp.rows,
                rowCount: resp.totalRowCount,
                queryTimeMs: resp.durationMs,
                query: resp.query,
                lastStatementSql: resp.lastStatementSql,
                statementCount: resp.statementCount,
                statementWithOutputCount: resp.statementWithOutputCount,
              },
          fillHeight: true,
          trace,
          onIdClick: (sqlTable, id, doubleClick) => {
            trace.navigate('#!/viewer');
            trace.selection.selectSqlEvent(sqlTable, id, {
              switchToCurrentSelectionTab: doubleClick,
              scrollToSelection: true,
            });
          },
        })
      : m(EmptyState, {
          title: 'Query results will appear here',
          fillHeight: true,
        });

    return m(SplitPanel, {
      direction: 'vertical',
      initialSplit: {percent: 50},
      minSize: 100,
      firstPanel: editorPanel,
      secondPanel: resultsPanel,
    });
  }

  private renderTablesTab(attrs: QueryPageAttrs): m.Children {
    const sqlModulesPlugin = attrs.trace.plugins.getPlugin(SqlModulesPlugin);
    const sqlModules = sqlModulesPlugin.getSqlModules();

    if (!sqlModules) {
      return m(
        EmptyState,
        {
          title: 'Loading tables...',
          icon: 'hourglass_empty',
          fillHeight: true,
        },
        m(Spinner),
      );
    }

    return m(TableList, {
      sqlModules,
      onQueryTable: (tableName, query) => {
        attrs.onTabAdd?.(tableName, query, true);
      },
    });
  }

  private shouldDisplayPerfettoSqlAgentBanner(attrs: QueryPageAttrs) {
    return (
      attrs.trace.isInternalUser &&
      localStorage.getItem(HIDE_PERFETTO_SQL_AGENT_BANNER_KEY) !== 'true'
    );
  }

  private hidePerfettoSqlAgentBanner() {
    localStorage.setItem(HIDE_PERFETTO_SQL_AGENT_BANNER_KEY, 'true');
  }

  private async formatSql(attrs: QueryPageAttrs, tabId: string, text: string) {
    try {
      const engine = await this.getFormatterEngine();
      const result = engine.runFmt(text, {
        lineWidth: 80,
        indentWidth: 2,
        keywordCase: 1,
        semicolons: true,
      });
      if (result.ok) {
        attrs.onEditorContentUpdate?.(tabId, result.text);
        m.redraw();
      } else {
        console.error('SQL formatting failed', result.text);
      }
    } catch (e) {
      console.error('SQL formatting failed', e);
    }
  }
}
