/**
 * ConversationTree is a pure data model for the conversation message tree.
 * It owns the message map, tree structure (parent/children), and localStorage
 * persistence. No DOM, no rendering.
 */

const STORAGE_KEY = 'odidere_history';

/**
 * extractText extracts plain text from a message content field.
 * Content can be a string or an array of content-part objects.
 * @param {string|Array} content
 * @returns {string}
 */
export function extractText(content) {
  if (typeof content === 'string') return content;
  if (Array.isArray(content)) {
    return content
      .filter((p) => p.type === 'text')
      .map((p) => p.text)
      .join('\n');
  }
  return '';
}

export class ConversationTree {
  constructor() {
    this.messagesMap = new Map();
    this.leafId = null;
    this.rootId = null;
  }

  /**
   * get returns a message by ID.
   * @param {string} id
   * @returns {Object|undefined}
   */
  get(id) {
    return this.messagesMap.get(id);
  }

  /**
   * getActivePath resolves the ordered array of messages from root to
   * leaf by walking parent pointers from leafId.
   * @returns {Object[]} ordered messages
   */
  getActivePath() {
    if (!this.leafId) return [];
    const path = [];
    let current = this.leafId;
    while (current) {
      const msg = this.messagesMap.get(current);
      if (!msg) break;
      path.push(msg);
      current = msg.parent;
    }
    path.reverse();
    return path;
  }

  /** @returns {number} number of messages in the tree */
  get size() {
    return this.messagesMap.size;
  }

  /**
   * append adds messages to the tree. Each new message is a child of
   * the previous one (or the current leaf for the first). Only the last
   * message receives the metadata. Mutates the passed-in message objects
   * in place (adds id, parent, children, meta) and stores the same
   * references in messagesMap. Returns the same messages, now enriched.
   * @param {Object[]} messages
   * @param {Object} [meta]
   * @returns {Object[]} the same message objects, now enriched
   */
  append(messages, meta = {}) {
    let prevId = this.leafId;
    for (let i = 0; i < messages.length; i++) {
      const msg = messages[i];

      // Generate ID if not present.
      if (!msg.id) {
        msg.id = crypto.randomUUID();
      }

      // Attach metadata only to the final message.
      if (i === messages.length - 1) {
        msg.meta = meta;
      }

      // Link to the tree.
      msg.parent = prevId;
      msg.children = [];

      if (prevId) {
        const parent = this.messagesMap.get(prevId);
        if (parent) {
          parent.children.push(msg.id);
        }
      } else {
        // First message in the conversation.
        this.rootId = msg.id;
      }

      this.messagesMap.set(msg.id, msg);
      this.leafId = msg.id;
      prevId = msg.id;
    }

    this.save();
    return messages;
  }

  /**
   * delete removes a message from the tree and reparents its children to
   * the grandparent. Also handles tool call / tool result pairing. When
   * deleting the root, the first child becomes the new root and any
   * remaining children are reparented under it.
   * @param {string} messageId
   */
  delete(messageId) {
    const msg = this.messagesMap.get(messageId);
    if (!msg) return;

    const parentId = msg.parent; // null if this is the root
    const parent = parentId ? this.messagesMap.get(parentId) : null;

    // Identify tool result messages that belong to this assistant message.
    const idsToDelete = new Set([messageId]);
    if (msg.role === 'assistant' && msg.tool_calls?.length > 0) {
      const toolCallIds = new Set(msg.tool_calls.map((tc) => tc.id));
      for (const [id, m] of this.messagesMap) {
        if (m.role === 'tool' && toolCallIds.has(m.tool_call_id)) {
          idsToDelete.add(id);
        }
      }
    }

    // Collect all children that need reparenting.
    const childrenToReparent = [];
    for (const deleteId of idsToDelete) {
      const delMsg = this.messagesMap.get(deleteId);
      if (delMsg?.children) {
        childrenToReparent.push(...delMsg.children);
      }
    }

    // Remove deleted messages from the map.
    for (const deleteId of idsToDelete) {
      this.messagesMap.delete(deleteId);
    }

    // Remove deleted IDs from parent's children.
    if (parent) {
      parent.children = parent.children.filter((c) => !idsToDelete.has(c));
    }

    // Reparent children of deleted messages to the grandparent.
    if (parent) {
      for (const childId of childrenToReparent) {
        const child = this.messagesMap.get(childId);
        if (child) {
          child.parent = parentId;
          if (!parent.children.includes(childId)) {
            parent.children.push(childId);
          }
        }
      }
    } else if (childrenToReparent.length > 0) {
      // Root deletion: first child becomes the new root.
      const newRootId = childrenToReparent[0];
      const newRoot = this.messagesMap.get(newRootId);
      if (newRoot) {
        newRoot.parent = null;
        for (let i = 1; i < childrenToReparent.length; i++) {
          const childId = childrenToReparent[i];
          const child = this.messagesMap.get(childId);
          if (child) {
            child.parent = newRootId;
            if (!newRoot.children.includes(childId)) {
              newRoot.children.push(childId);
            }
          }
        }
      }
      this.rootId = newRootId;
    } else {
      this.rootId = null;
    }

    // Update leafId if it was deleted.
    if (idsToDelete.has(this.leafId)) {
      if (parentId) {
        this.updateLeafId(parentId);
      } else if (childrenToReparent.length > 0) {
        this.updateLeafId(childrenToReparent[0]);
      } else {
        this.leafId = null;
      }
    }

    this.save();
  }

  /**
   * updateLeafId walks the tree from a starting node to find the deepest
   * leaf by following the first child at each step.
   * @param {string} startId
   */
  updateLeafId(startId) {
    let current = startId;
    while (current) {
      const msg = this.messagesMap.get(current);
      if (!msg?.children || msg.children.length === 0) break;
      current = msg.children[0];
    }
    this.leafId = current;
  }

  /**
   * load reads chat history from localStorage and populates the map.
   */
  load() {
    try {
      const stored = localStorage.getItem(STORAGE_KEY);
      if (!stored) return;

      const parsed = JSON.parse(stored);

      // Ensure messages have parent/children fields (defensive).
      for (const msg of parsed.messages) {
        if (msg.parent === undefined) msg.parent = null;
        if (msg.children === undefined) msg.children = [];
      }

      this.messagesMap = new Map(parsed.messages.map((m) => [m.id, m]));
      this.leafId = parsed.leafId;
      this.rootId = parsed.rootId;
    } catch (e) {
      console.error('failed to load history:', e);
      this.messagesMap.clear();
      this.leafId = null;
      this.rootId = null;
    }
  }

  /**
   * save persists the tree to localStorage.
   */
  save() {
    const messages = Array.from(this.messagesMap.values());
    const data = {
      messages,
      leafId: this.leafId,
      rootId: this.rootId,
    };
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
    } catch (e) {
      console.error('failed to save history:', e);
    }
  }

  /**
   * clear wipes the map and localStorage.
   */
  clear() {
    this.messagesMap.clear();
    this.leafId = null;
    this.rootId = null;
    localStorage.removeItem(STORAGE_KEY);
  }
}
