// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited

import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import * as vscode from "vscode";
import { log } from "./output.js";

/**
 * Sidebar TreeView showing indexed projects discovered from the engine's
 * temp directory. The engine writes interactive graph HTML to
 * `os.tmpdir()/socraticode-graph/<projectId>.html` whenever the user (or
 * their AI assistant) calls `codebase_graph_visualize`. Listing those
 * files gives a low-cost, no-IPC view of "what has SocratiCode worked on
 * recently".
 *
 * For richer state (current index size, embedding count, freshness) the
 * view will eventually call into the running engine via `vscode.lm.tools`,
 * but that requires a stable invokeTool API across MCP hosts. Until then,
 * the welcome view + the file listing are enough to get users oriented.
 */

interface ProjectItem {
  projectId: string;
  graphPath?: string;
  mtime?: Date;
}

const GRAPH_DIR = path.join(os.tmpdir(), "socraticode-graph");

export class ProjectsTreeProvider implements vscode.TreeDataProvider<ProjectItem> {
  private readonly _onDidChange = new vscode.EventEmitter<ProjectItem | undefined>();
  readonly onDidChangeTreeData = this._onDidChange.event;

  refresh(): void {
    this._onDidChange.fire(undefined);
  }

  getTreeItem(element: ProjectItem): vscode.TreeItem {
    const item = new vscode.TreeItem(element.projectId, vscode.TreeItemCollapsibleState.None);
    item.iconPath = new vscode.ThemeIcon("symbol-namespace");
    if (element.mtime) {
      item.description = `graph generated ${formatRelative(element.mtime)}`;
    } else {
      item.description = "no graph yet";
    }
    item.tooltip = element.graphPath
      ? `Click to open the interactive graph for ${element.projectId}.\nFile: ${element.graphPath}`
      : `No interactive graph cached for ${element.projectId}.`;
    if (element.graphPath) {
      item.command = {
        command: "socraticode.openInteractiveGraph",
        title: "Open interactive graph",
        arguments: [element.projectId],
      };
    }
    item.contextValue = element.graphPath ? "project.withGraph" : "project";
    return item;
  }

  async getChildren(): Promise<ProjectItem[]> {
    try {
      const entries = await fs.readdir(GRAPH_DIR, { withFileTypes: true });
      const items: ProjectItem[] = [];
      for (const entry of entries) {
        if (!entry.isFile() || !entry.name.endsWith(".html")) continue;
        const projectId = entry.name.replace(/\.html$/, "");
        const graphPath = path.join(GRAPH_DIR, entry.name);
        try {
          const stat = await fs.stat(graphPath);
          items.push({ projectId, graphPath, mtime: stat.mtime });
        } catch {
          items.push({ projectId, graphPath });
        }
      }
      // Most recently generated first. When mtime is missing on a side,
      // push that side to the end. When both are missing, fall back to
      // a stable lexicographic tiebreak on projectId so the ordering is
      // deterministic regardless of fs.readdir() traversal order.
      items.sort((a, b) => {
        if (!a.mtime && !b.mtime) return a.projectId.localeCompare(b.projectId);
        if (!a.mtime) return 1;
        if (!b.mtime) return -1;
        return b.mtime.getTime() - a.mtime.getTime();
      });
      return items;
    } catch (err) {
      const e = err as NodeJS.ErrnoException;
      if (e.code === "ENOENT") {
        // Engine has never generated a graph yet. The welcome view in
        // package.json handles the empty state.
        return [];
      }
      log(`Sidebar: failed to read ${GRAPH_DIR}: ${e.message}`);
      return [];
    }
  }
}

export function registerSidebar(context: vscode.ExtensionContext): ProjectsTreeProvider {
  const provider = new ProjectsTreeProvider();
  const view = vscode.window.createTreeView("socraticode.projects", {
    treeDataProvider: provider,
    showCollapseAll: false,
  });
  context.subscriptions.push(view);
  return provider;
}

function formatRelative(date: Date): string {
  // Clamp to zero so a file mtime slightly ahead of the local clock
  // (NTP correction, container clock skew) doesn't render "-5s ago".
  const seconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));
  if (seconds < 60) return `${seconds}s ago`;
  const minutes = Math.round(seconds / 60);
  if (minutes < 60) return `${minutes}m ago`;
  const hours = Math.round(minutes / 60);
  if (hours < 24) return `${hours}h ago`;
  const days = Math.round(hours / 24);
  return `${days}d ago`;
}
