import { Effect, Layer, Option, Ref, Stream } from "effect";

import refreshPackagesCommand from "../../commands/refreshPackages.ts";
import { NotebookRuntime } from "../../kernel/NotebookRuntime.ts";
import { NotebookEditorRegistry } from "../../notebook/NotebookEditorRegistry.ts";
import { VsCode } from "../../platform/VsCode.ts";
import type { NotebookId } from "../../schemas/MarimoNotebookDocument.ts";
import type { DependencyTreeNode } from "../../schemas/Models.gen.ts";
import { TreeView } from "../TreeView.ts";
import { PackagesService } from "./PackagesService.ts";

interface PackageTreeItem {
  type: "package";
  notebookUri: NotebookId;
  name: string;
  version: string | null;
  tags: readonly Record<string, string>[];
  dependencies: readonly DependencyTreeNode[];
}

/**
 * Manages the packages tree view for the active notebook.
 *
 * Displays a hierarchical view of package dependencies as a tree.
 *
 * Subscribes to package dependency tree changes and updates the view in real-time.
 */
export const PackagesViewLive = Layer.scopedDiscard(
  Effect.gen(function* () {
    const treeView = yield* TreeView;
    const packagesService = yield* PackagesService;
    const editorRegistry = yield* NotebookEditorRegistry;
    const notebooks = yield* NotebookRuntime;
    const code = yield* VsCode;

    // Track the current package tree items for the active notebook
    const packageItems = yield* Ref.make<readonly PackageTreeItem[]>([]);

    // Create the tree data provider
    const provider = yield* treeView.createTreeDataProvider({
      viewId: "marimo-explorer-packages",
      getChildren: (element?: PackageTreeItem) =>
        Effect.gen(function* () {
          if (!element) {
            // Root level: return top-level packages
            const items = yield* Ref.get(packageItems);
            return [...items];
          }

          // Return dependencies of this package
          const notebookUri = element.notebookUri;
          return element.dependencies.map((dep) => ({
            type: "package" as const,
            notebookUri,
            name: dep.name,
            version: dep.version,
            tags: dep.tags,
            dependencies: dep.dependencies,
          }));
        }),
      getTreeItem: (element: PackageTreeItem) =>
        Effect.succeed({
          label: element.name,
          description: element.version ?? undefined,
          tooltip: `${element.name}${element.version ? `@${element.version}` : ""}${
            element.tags.length > 0
              ? `\n${element.tags
                  .map((t) =>
                    Object.entries(t)
                      .map(([k, v]) => `${k}: ${v}`)
                      .join(", "),
                  )
                  .join("\n")}`
              : ""
          }`,
          iconPath: undefined,
          contextValue: "marimoPackage",
          collapsibleState:
            element.dependencies.length > 0
              ? ("Collapsed" as const)
              : ("None" as const),
        }),
    });

    // Helper to rebuild the package tree from current state
    const refreshPackages = Effect.fn(function* () {
      const activeNotebookUri = yield* editorRegistry.getActiveNotebookUri();

      yield* Effect.logDebug("Refreshing packages").pipe(
        Effect.annotateLogs({
          activeNotebookUri: Option.getOrElse(activeNotebookUri, () => null),
        }),
      );
      if (Option.isNone(activeNotebookUri)) {
        yield* Ref.set(packageItems, []);
        yield* provider.refresh();
        return;
      }

      const notebookUri = activeNotebookUri.value;

      // Check if we already have the dependency tree cached
      const cachedTreeState =
        yield* packagesService.getDependencyTree(notebookUri);

      if (Option.isNone(cachedTreeState)) {
        // No cached tree, fetch it
        yield* packagesService.fetchDependencyTree(notebookUri);
        // Will be updated via the stream subscription
        return;
      }

      const { tree, loading, error } = cachedTreeState.value;

      if (loading || error || !tree) {
        yield* Ref.set(packageItems, []);
        yield* provider.refresh();
        return;
      }

      // Surface the user's direct dependencies and hide the tree's top-level
      // wrapper. Across all paths the root is uninformative:
      //   - `<root>` for PEP 723 script mode
      //   - the uv project name when the notebook lives in a uv project
      //   - `installed-packages` from our venv pip-list fallback synthesizer
      // The deps below it are what the user actually wants to see.
      const items: PackageTreeItem[] = tree.dependencies.map((dep) => ({
        type: "package",
        notebookUri,
        name: dep.name,
        version: dep.version,
        tags: dep.tags,
        dependencies: dep.dependencies,
      }));

      yield* Effect.logDebug("Refreshed packages").pipe(
        Effect.annotateLogs({
          topLevelCount: items.length,
        }),
      );
      yield* Ref.set(packageItems, items);
      yield* provider.refresh();
    });

    // Subscribe to active notebook changes
    yield* Effect.forkScoped(
      editorRegistry.streamActiveNotebookChanges().pipe(
        Stream.runForEach(() => {
          return refreshPackages();
        }),
      ),
    );

    // Subscribe to dependency tree changes
    yield* Effect.forkScoped(
      packagesService.streamDependencyTreeChanges().pipe(
        Stream.runForEach(
          Effect.fn(function* (_treeMap) {
            yield* refreshPackages();
          }),
        ),
      ),
    );

    // Invalidate cache when the user switches kernels on a notebook —
    // the cached tree was computed against the old env. Clearing fires
    // streamDependencyTreeChanges, which triggers refreshPackages to
    // re-fetch with the new controller's `target`.
    yield* Effect.forkScoped(
      notebooks.controllerChanges().pipe(
        Stream.runForEach(
          Effect.fn(function* ({ notebookUri }) {
            yield* packagesService.clearNotebook(notebookUri);
          }),
        ),
      ),
    );

    // Register command to refresh packages
    yield* code.commands.register(refreshPackagesCommand);

    yield* Effect.logDebug("Packages view initialized");
  }),
);
