// Copyright (C) 2026 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 {z} from 'zod';
import {assertTrue} from '../../base/assert';
import {Card, CardStack} from '../../widgets/card';
import {Button} from '../../widgets/button';
import {Icons} from '../../base/semantic_icons';
import type {DataExplorerState} from './data_explorer';
import {serializeState} from './json_handler';
import {getAllNodes} from './query_builder/graph_utils';

const RECENT_GRAPHS_KEY = 'recentExploreGraphs';

// Schema for a single recent graph entry
const RECENT_GRAPH_ENTRY_SCHEMA = z.object({
  name: z.string(),
  json: z.string(),
  timestamp: z.number(),
  nodeCount: z.number().optional(),
  labelCount: z.number().optional(),
  starred: z.boolean().default(false),
});

export type RecentGraphEntry = z.infer<typeof RECENT_GRAPH_ENTRY_SCHEMA>;

const RECENT_GRAPHS_SCHEMA = z.array(RECENT_GRAPH_ENTRY_SCHEMA);

export type RecentGraphs = z.infer<typeof RECENT_GRAPHS_SCHEMA>;

/**
 * Storage class for recent data explorer graphs.
 * Stores serialized graph states in localStorage.
 *
 * The data array uses index 0 as a "working slot" for the current graph being
 * edited. When the user switches to a different graph (via import, example, or
 * clear), the current graph is "finalized" - it gets a proper name and becomes
 * a historical entry at index 1+. A new empty placeholder is then inserted at
 * index 0 for the next graph.
 *
 * Historical graphs (index 1+) are displayed in the Recent Graphs section.
 * The working slot (index 0) is never displayed to the user.
 */
export class RecentGraphsStorage {
  private _data: RecentGraphs;
  maxItems = 10;

  constructor() {
    this._data = this.load();
  }

  /**
   * Returns the recent graphs data. Historical graphs start at index 1.
   * Index 0 is the current working graph (not displayed in UI).
   */
  get data(): RecentGraphs {
    return this._data;
  }

  /**
   * Sets the data directly. Used for testing.
   */
  set data(value: RecentGraphs) {
    this._data = value;
  }

  /**
   * Saves the current graph state. This updates the working slot (index 0),
   * or creates it if the list is empty.
   * Called on every state change to persist the current work.
   */
  saveCurrentState(state: DataExplorerState): void {
    // Don't save empty graphs
    if (state.rootNodes.length === 0) {
      return;
    }

    const json = serializeState(state);
    const timestamp = Date.now();
    const nodeCount = getAllNodes(state.rootNodes).length;
    const labelCount = state.labels.length;

    if (this._data.length === 0) {
      // No entries yet - create the working slot
      this._data.unshift({
        name: this.generateName(),
        json,
        timestamp,
        nodeCount,
        labelCount,
        starred: false,
      });
    } else {
      // Update the working slot in place
      this._data[0].json = json;
      this._data[0].timestamp = timestamp;
      this._data[0].nodeCount = nodeCount;
      this._data[0].labelCount = labelCount;
    }
    this.save();
  }

  /**
   * Finalizes the current graph and prepares for a new one.
   * Called when the user switches to a different graph (New graph, tutorial, etc.).
   * The working slot (index 0) becomes a historical entry (moved to index 1+).
   */
  finalizeCurrentGraph(): void {
    const hasWorkingGraph =
      this._data.length > 0 && (this._data[0].nodeCount ?? 0) > 0;
    if (!hasWorkingGraph) {
      return;
    }

    // Give the current graph a proper name based on when it was finalized
    this._data[0].name = this.generateName();

    // Count unstarred items (excluding working slot which will become historical)
    let lastUnstarredIndex = -1;
    let unstarredCount = 0;
    for (let i = 0; i < this._data.length; i++) {
      if (!this._data[i].starred) {
        unstarredCount++;
        lastUnstarredIndex = i;
      }
    }

    // If we're at max unstarred capacity, remove the oldest unstarred
    // (but not the working slot at index 0)
    if (unstarredCount >= this.maxItems && lastUnstarredIndex > 0) {
      this._data.splice(lastUnstarredIndex, 1);
    }

    // Insert a new empty working slot at index 0
    // This pushes the finalized graph to index 1
    this._data.unshift({
      name: 'Current',
      json: '',
      timestamp: Date.now(),
      nodeCount: 0,
      starred: false,
    });

    this.save();
  }

  /**
   * Gets the most recent valid graph's JSON for restoring state on page load.
   * Skips empty working slots and returns the first graph with actual content.
   */
  getCurrentJson(): string | undefined {
    for (const entry of this._data) {
      if (entry.json && (entry.nodeCount ?? 0) > 0) {
        return entry.json;
      }
    }
    return undefined;
  }

  /**
   * Sets the starred status of a graph.
   */
  setStarred(index: number, starred: boolean): void {
    assertTrue(index >= 0 && index < this._data.length);
    this._data[index].starred = starred;
    this.save();
  }

  /**
   * Renames a graph.
   */
  rename(index: number, newName: string): void {
    assertTrue(index >= 0 && index < this._data.length);
    this._data[index].name = newName.trim() || this._data[index].name;
    this.save();
  }

  /**
   * Generates a unique name for a new graph based on timestamp.
   */
  generateName(): string {
    const now = new Date();
    const dateStr = now.toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric',
    });
    const timeStr = now.toLocaleTimeString('en-US', {
      hour: '2-digit',
      minute: '2-digit',
      hour12: false,
    });
    return `Graph ${dateStr} ${timeStr}`;
  }

  /**
   * Gets a graph's JSON by index.
   */
  getJson(index: number): string | undefined {
    if (index >= 0 && index < this._data.length) {
      return this._data[index].json;
    }
    return undefined;
  }

  /**
   * Removes a graph from the list by index.
   */
  remove(index: number): void {
    assertTrue(index >= 0 && index < this._data.length);
    this._data.splice(index, 1);
    this.save();
  }

  /**
   * Clears all stored data. Used when stored data is corrupted.
   */
  clear(): void {
    this._data = [];
    this.save();
  }

  private load(): RecentGraphs {
    const value = window.localStorage.getItem(RECENT_GRAPHS_KEY);
    if (value === null) {
      return [];
    }
    try {
      const res = RECENT_GRAPHS_SCHEMA.safeParse(JSON.parse(value));
      return res.success ? res.data : [];
    } catch {
      // Invalid JSON in localStorage, return empty array
      return [];
    }
  }

  private save(): void {
    try {
      window.localStorage.setItem(
        RECENT_GRAPHS_KEY,
        JSON.stringify(this._data),
      );
    } catch (e) {
      // Handle localStorage quota exceeded or other storage errors.
      // Log the error but don't crash - recent graphs is a nice-to-have feature.
      console.warn('Failed to save recent graphs to localStorage:', e);
    }
  }
}

// Singleton instance
export const recentGraphsStorage = new RecentGraphsStorage();

export interface RecentGraphsSectionAttrs {
  readonly onLoadGraph: (json: string) => void;
}

interface RecentGraphCardAttrs {
  readonly entry: RecentGraphEntry;
  readonly index: number;
  readonly onLoadGraph: (json: string) => void;
}

/**
 * Component that renders a single recent graph card.
 */
class RecentGraphCard implements m.ClassComponent<RecentGraphCardAttrs> {
  private isEditing = false;
  private editName = '';

  private startEditing(entry: RecentGraphEntry, e: Event): void {
    e.stopPropagation();
    this.isEditing = true;
    this.editName = entry.name;
  }

  private finishEditing(index: number): void {
    if (this.editName.trim()) {
      recentGraphsStorage.rename(index, this.editName);
    }
    this.isEditing = false;
  }

  private cancelEditing(): void {
    this.isEditing = false;
  }

  view({attrs}: m.CVnode<RecentGraphCardAttrs>): m.Children {
    const {entry, index} = attrs;

    if (this.isEditing) {
      return m(
        Card,
        {
          className: 'pf-recent-graph-card',
          onclick: (e: Event) => e.stopPropagation(),
        },
        m(
          'div',
          m('input', {
            type: 'text',
            value: this.editName,
            oninput: (e: Event) => {
              this.editName = (e.target as HTMLInputElement).value;
            },
            onkeydown: (e: KeyboardEvent) => {
              if (e.key === 'Enter') {
                e.preventDefault();
                this.finishEditing(index);
              } else if (e.key === 'Escape') {
                e.preventDefault();
                this.cancelEditing();
              }
            },
            oncreate: (vnode: m.VnodeDOM<unknown>) => {
              (vnode.dom as HTMLInputElement).focus();
              (vnode.dom as HTMLInputElement).select();
            },
          }),
          m('p', `${entry.nodeCount ?? '?'} nodes`),
        ),
        m(
          '.pf-recent-graph-card__actions',
          m(Button, {
            icon: 'check',
            title: 'Save',
            onclick: (e: Event) => {
              e.stopPropagation();
              this.finishEditing(index);
            },
          }),
          m(Button, {
            icon: 'close',
            title: 'Cancel',
            onclick: (e: Event) => {
              e.stopPropagation();
              this.cancelEditing();
            },
          }),
        ),
      );
    }

    const nodeCount = entry.nodeCount ?? 0;
    const labelCount = entry.labelCount ?? 0;

    return m(
      Card,
      {
        interactive: true,
        className: 'pf-recent-graph-card',
        onclick: () => {
          const json = recentGraphsStorage.getJson(index);
          if (json !== undefined) {
            attrs.onLoadGraph(json);
          }
        },
      },
      m(Button, {
        icon: Icons.Star,
        iconFilled: entry.starred,
        title: entry.starred ? 'Unstar' : 'Star',
        onclick: (e: Event) => {
          e.stopPropagation();
          recentGraphsStorage.setStarred(index, !entry.starred);
        },
      }),
      m(
        'div',
        m('h3', entry.name),
        m('p', `${nodeCount} nodes, ${labelCount} labels`),
      ),
      m(
        '.pf-recent-graph-card__actions',
        m(Button, {
          icon: 'edit',
          title: 'Rename',
          onclick: (e: Event) => this.startEditing(entry, e),
        }),
        m(Button, {
          icon: Icons.Delete,
          title: 'Remove from recent',
          onclick: (e: Event) => {
            e.stopPropagation();
            recentGraphsStorage.remove(index);
          },
        }),
      ),
    );
  }
}

/**
 * Component that renders the "Recent graphs" section in the sidebar.
 * Shows historical graphs (not the current one at index 0).
 * Starred graphs appear first, followed by unstarred graphs.
 * Always shows the section header, even when empty.
 */
export class RecentGraphsSection
  implements m.ClassComponent<RecentGraphsSectionAttrs>
{
  view({attrs}: m.CVnode<RecentGraphsSectionAttrs>): m.Children {
    const recentGraphs = recentGraphsStorage.data;

    // Separate starred and unstarred graphs while preserving indices
    // Start from index 1 to skip the current graph (index 0)
    const starred: Array<{entry: RecentGraphEntry; index: number}> = [];
    const unstarred: Array<{entry: RecentGraphEntry; index: number}> = [];

    for (let i = 1; i < recentGraphs.length; i++) {
      const entry = recentGraphs[i];
      // Skip empty placeholders (working slots with no content)
      if ((entry.nodeCount ?? 0) === 0) continue;

      if (entry.starred) {
        starred.push({entry, index: i});
      } else {
        unstarred.push({entry, index: i});
      }
    }

    // Render starred first, then unstarred
    const allCards = [...starred, ...unstarred];

    return m(
      '.pf-recent-graphs-section.pf-nav-section',
      m('.pf-nav-section-header', m('span', 'Recent graphs')),
      allCards.length > 0
        ? m(
            '.pf-recent-graphs-scroll-container',
            m(
              CardStack,
              allCards.map(({entry, index}) =>
                m(RecentGraphCard, {
                  key: index,
                  entry,
                  index,
                  onLoadGraph: attrs.onLoadGraph,
                }),
              ),
            ),
          )
        : m('.pf-recent-graphs-empty', 'No recent graphs'),
    );
  }
}
