/**
 * Zarr hierarchy traversal — build a tree of groups and arrays.
 *
 * @typedef {Object} TreeNode
 * @property {string} path - Absolute path (e.g. "/", "/group1/array1")
 * @property {"group"|"array"} kind - Node type
 * @property {TreeNode[]} children - Child nodes (empty for arrays)
 * @property {Record<string, any>} attrs - Node attributes
 * @property {number[]|undefined} shape - Array shape (arrays only)
 * @property {string|undefined} dtype - Array data type (arrays only)
 * @property {any|undefined} chunks - Array chunk configuration (arrays only)
 * @property {object|null|undefined} meta - Raw metadata (.zarray for v2, zarr.json for v3)
 */

import { root, open } from "zarrita";

/**
 * Build a full hierarchy tree from a consolidated (Listable) store.
 *
 * @param {object} listableStore - A Listable store with .contents() method
 * @param {2|3|null} [zarrFormat] - Known zarr format to skip version probing
 * @returns {Promise<TreeNode>}
 */
export async function buildTree(listableStore, zarrFormat = null) {
  const contents = listableStore.contents();
  return buildTreeFromContents(listableStore, contents, zarrFormat);
}

/**
 * Build a hierarchy tree from crawled directory entries.
 *
 * For non-consolidated stores where HTML directory crawling discovered the
 * hierarchy, this opens each discovered node to get its metadata.
 *
 * @param {object} store - A zarrita store (FetchStore)
 * @param {import("./crawl.js").CrawlEntry[]} entries - Crawled entries
 * @returns {Promise<TreeNode>}
 */
/**
 * Build a hierarchy tree from v3 inline consolidated metadata.
 *
 * This is the fast path — all metadata is already available from the root
 * zarr.json, so no additional HTTP requests are needed.
 *
 * @param {import("./consolidated-v3.js").V3NodeEntry[]} v3Entries
 * @returns {TreeNode}
 */
export function buildTreeFromV3(v3Entries) {
  /** @type {Map<string, TreeNode>} */
  const nodes = new Map();

  for (const entry of v3Entries) {
    /** @type {TreeNode} */
    const treeNode = {
      path: entry.path,
      kind: entry.kind,
      children: [],
      attrs: entry.attrs,
      meta: entry.meta || null,
    };

    // Extract array metadata from the v3 metadata object if available
    if (entry.kind === "array" && entry.meta) {
      if (entry.meta.shape) treeNode.shape = entry.meta.shape;
      if (entry.meta.data_type) treeNode.dtype = entry.meta.data_type;
      if (entry.meta.chunk_grid) treeNode.chunks = entry.meta.chunk_grid;
    }

    nodes.set(entry.path, treeNode);
  }

  // Ensure root exists
  if (!nodes.has("/")) {
    nodes.set("/", { path: "/", kind: "group", children: [], attrs: {} });
  }

  // Build parent-child relationships
  for (const [path, node] of nodes) {
    if (path === "/") continue;
    const parentPath = getParentPath(path);
    const parent = nodes.get(parentPath);
    if (parent) {
      parent.children.push(node);
    }
  }

  // Sort children
  for (const node of nodes.values()) {
    node.children.sort((a, b) => a.path.localeCompare(b.path));
  }

  return nodes.get("/");
}

export async function buildTreeFromCrawl(store, entries) {
  // Detect zarr format from the first entry — all nodes in a store share
  // the same format, so we can skip probing the wrong version for every node.
  const zarrFormat = entries[0]?.zarrFormat ?? null;

  const contents = entries.map((e) => ({
    path: e.path,
    kind: e.kind === "unknown" ? undefined : e.kind,
  }));
  return buildTreeFromContents(store, contents, zarrFormat);
}

/**
 * @param {object} store
 * @param {{ path: string, kind?: string }[]} contents
 * @param {2|3|null} [zarrFormat] - Known zarr format to skip version probing
 * @returns {Promise<TreeNode>}
 */
async function buildTreeFromContents(store, contents, zarrFormat = null) {

  // Create a flat map of path → TreeNode
  /** @type {Map<string, TreeNode>} */
  const nodes = new Map();

  // Open all nodes in parallel to get attributes and array metadata
  const opened = await Promise.all(
    contents.map((entry) => openNodeFromStore(store, entry.path, entry.kind, zarrFormat)),
  );
  for (let i = 0; i < contents.length; i++) {
    nodes.set(contents[i].path, opened[i]);
  }

  // Ensure root exists
  if (!nodes.has("/")) {
    nodes.set("/", {
      path: "/",
      kind: "group",
      children: [],
      attrs: {},
    });
  }

  // Build parent-child relationships
  for (const [path, node] of nodes) {
    if (path === "/") continue;
    const parentPath = getParentPath(path);
    const parent = nodes.get(parentPath);
    if (parent) {
      parent.children.push(node);
    }
  }

  // Sort children alphabetically by path
  for (const node of nodes.values()) {
    node.children.sort((a, b) => a.path.localeCompare(b.path));
  }

  return nodes.get("/");
}

/**
 * Open a single node at an arbitrary path. Use this for non-consolidated
 * stores where paths must be added manually.
 *
 * @param {object} store - A zarrita store (FetchStore or Listable)
 * @param {string} path - Path within the store (e.g. "/mygroup/myarray")
 * @returns {Promise<TreeNode>}
 */
export async function openNode(store, path) {
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
  return openNodeFromStore(store, normalizedPath);
}

/**
 * Insert a child node into an existing tree at the correct position.
 *
 * @param {TreeNode} root - Root of the tree
 * @param {TreeNode} childNode - Node to insert
 * @returns {TreeNode} The root node (mutated in place)
 */
export function insertNode(root, childNode) {
  const parentPath = getParentPath(childNode.path);
  const parent = findNode(root, parentPath);
  if (parent && parent.kind === "group") {
    // Avoid duplicates
    if (!parent.children.some((c) => c.path === childNode.path)) {
      parent.children.push(childNode);
      parent.children.sort((a, b) => a.path.localeCompare(b.path));
    }
  }
  return root;
}

/**
 * Find a node in the tree by path.
 *
 * @param {TreeNode} root
 * @param {string} path
 * @returns {TreeNode|undefined}
 */
export function findNode(root, path) {
  if (root.path === path) return root;
  for (const child of root.children) {
    const found = findNode(child, path);
    if (found) return found;
  }
  return undefined;
}

/**
 * Read the raw metadata file (.zarray or zarr.json) from the store.
 * For consolidated stores, this hits the in-memory cache (no network request).
 *
 * @param {object} store - A zarrita store with a get() method
 * @param {string} path - Node path (e.g. "/", "/group1/array1")
 * @param {2|3|null} [zarrFormat] - Known format to avoid probing both
 * @returns {Promise<object|null>} Parsed metadata object, or null
 */
async function readRawMeta(store, path, zarrFormat = null) {
  const prefix = path === "/" ? "" : path;

  if (zarrFormat !== 2) {
    try {
      const v3Bytes = await store.get(`${prefix}/zarr.json`);
      if (v3Bytes) {
        return JSON.parse(new TextDecoder().decode(v3Bytes));
      }
    } catch { /* ignore */ }
  }

  if (zarrFormat !== 3) {
    try {
      const v2Bytes = await store.get(`${prefix}/.zarray`);
      if (v2Bytes) {
        return JSON.parse(new TextDecoder().decode(v2Bytes));
      }
    } catch { /* ignore */ }
  }

  return null;
}

/**
 * @param {object} store
 * @param {string} path
 * @param {"group"|"array"} [hintKind]
 * @param {2|3|null} [zarrFormat] - Known zarr format to avoid version probing
 * @returns {Promise<TreeNode>}
 */
async function openNodeFromStore(store, path, hintKind, zarrFormat = null) {
  const location = root(store).resolve(path);
  const opts = hintKind ? { kind: hintKind } : {};

  try {
    // Use format-specific open to avoid probing the wrong version
    let node;
    if (zarrFormat === 2) {
      node = await open.v2(location, opts);
    } else if (zarrFormat === 3) {
      node = await open.v3(location, opts);
    } else {
      node = await open(location, opts);
    }

    /** @type {TreeNode} */
    const treeNode = {
      path,
      kind: node.kind,
      children: [],
      attrs: node.attrs || {},
    };

    if (node.kind === "array") {
      treeNode.shape = node.shape;
      treeNode.dtype = node.dtype;
      treeNode.chunks = node.chunks;
      treeNode.meta = await readRawMeta(store, path, zarrFormat);
    }

    return treeNode;
  } catch (err) {
    // Return a minimal node if we can't open it
    return {
      path,
      kind: hintKind || "group",
      children: [],
      attrs: {},
      _error: err.message,
    };
  }
}

/**
 * Get the parent path of a given path.
 * @param {string} path
 * @returns {string}
 */
function getParentPath(path) {
  if (path === "/") return "/";
  const parts = path.split("/").filter(Boolean);
  parts.pop();
  return parts.length === 0 ? "/" : `/${parts.join("/")}`;
}
