import type { InstanceSummary, TodoAction, TodoInstance, TodoItem } from "./types";

function cloneTodo(todo: TodoItem): TodoItem {
  return {
    ...todo,
    children: todo.children.map(cloneTodo)
  };
}

function makeId(prefix: string): string {
  return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
}

export function createTodo(title: string, now: string): TodoItem {
  return {
    id: makeId("todo"),
    title,
    completed: false,
    expanded: true,
    reminderAt: null,
    createdAt: now,
    updatedAt: now,
    children: []
  };
}

export function createInstance(name: string, filePath: string, historyPath: string, now: string): TodoInstance {
  return {
    id: makeId("instance"),
    name,
    filePath,
    historyPath,
    createdAt: now,
    updatedAt: now,
    historyPointer: 0,
    todos: []
  };
}

function updateNodeList(
  nodes: TodoItem[],
  todoId: string,
  updater: (todo: TodoItem) => TodoItem
): { nodes: TodoItem[]; found: boolean } {
  let found = false;
  const nextNodes = nodes.map((node) => {
    if (node.id === todoId) {
      found = true;
      return updater(node);
    }

    const nested = updateNodeList(node.children, todoId, updater);
    if (nested.found) {
      found = true;
      return {
        ...node,
        children: nested.nodes
      };
    }

    return node;
  });

  return { nodes: nextNodes, found };
}

function appendNode(nodes: TodoItem[], parentId: string, todo: TodoItem): { nodes: TodoItem[]; found: boolean } {
  let found = false;
  const nextNodes = nodes.map((node) => {
    if (node.id === parentId) {
      found = true;
      return {
        ...node,
        expanded: true,
        children: [...node.children, todo]
      };
    }

    const nested = appendNode(node.children, parentId, todo);
    if (nested.found) {
      found = true;
      return {
        ...node,
        children: nested.nodes
      };
    }

    return node;
  });

  return { nodes: nextNodes, found };
}

function removeNode(nodes: TodoItem[], todoId: string): { nodes: TodoItem[]; found: boolean } {
  let found = false;
  const nextNodes: TodoItem[] = [];

  for (const node of nodes) {
    if (node.id === todoId) {
      found = true;
      continue;
    }

    const nested = removeNode(node.children, todoId);
    if (nested.found) {
      found = true;
      nextNodes.push({
        ...node,
        children: nested.nodes
      });
      continue;
    }

    nextNodes.push(node);
  }

  return { nodes: nextNodes, found };
}

function containsTodo(nodes: TodoItem[], todoId: string): boolean {
  return nodes.some((node) => node.id === todoId || containsTodo(node.children, todoId));
}

function clampIndex(index: number, max: number): number {
  return Math.max(0, Math.min(index, max));
}

function removeNodeWithLocation(
  nodes: TodoItem[],
  todoId: string,
  parentId: string | null = null
): { nodes: TodoItem[]; removed: TodoItem | null; parentId: string | null; index: number } {
  for (let index = 0; index < nodes.length; index += 1) {
    const node = nodes[index];
    if (node.id === todoId) {
      return {
        nodes: [...nodes.slice(0, index), ...nodes.slice(index + 1)],
        removed: node,
        parentId,
        index
      };
    }

    const nested = removeNodeWithLocation(node.children, todoId, node.id);
    if (nested.removed) {
      return {
        nodes: [...nodes.slice(0, index), { ...node, children: nested.nodes }, ...nodes.slice(index + 1)],
        removed: nested.removed,
        parentId: nested.parentId,
        index: nested.index
      };
    }
  }

  return {
    nodes,
    removed: null,
    parentId: null,
    index: -1
  };
}

function insertNode(
  nodes: TodoItem[],
  parentId: string | null,
  index: number,
  todo: TodoItem,
  now: string
): { nodes: TodoItem[]; inserted: boolean } {
  const movedTodo = {
    ...todo,
    updatedAt: now
  };

  if (!parentId) {
    const nextNodes = [...nodes];
    nextNodes.splice(clampIndex(index, nextNodes.length), 0, movedTodo);
    return { nodes: nextNodes, inserted: true };
  }

  let inserted = false;
  const nextNodes = nodes.map((node) => {
    if (node.id === parentId) {
      inserted = true;
      const nextChildren = [...node.children];
      nextChildren.splice(clampIndex(index, nextChildren.length), 0, movedTodo);
      return {
        ...node,
        expanded: true,
        updatedAt: now,
        children: nextChildren
      };
    }

    const nested = insertNode(node.children, parentId, index, todo, now);
    if (nested.inserted) {
      inserted = true;
      return {
        ...node,
        children: nested.nodes
      };
    }

    return node;
  });

  return { nodes: nextNodes, inserted };
}

function setExpandedState(nodes: TodoItem[], expanded: boolean, now: string): TodoItem[] {
  return nodes.map((node) => {
    if (node.children.length === 0) {
      return node;
    }

    const nextChildren = setExpandedState(node.children, expanded, now);
    return {
      ...node,
      expanded,
      updatedAt: now,
      children: nextChildren
    };
  });
}

export function countTodos(nodes: TodoItem[]): { total: number; completed: number } {
  return nodes.reduce(
    (accumulator, node) => {
      const nested = countTodos(node.children);
      return {
        total: accumulator.total + 1 + nested.total,
        completed: accumulator.completed + (node.completed ? 1 : 0) + nested.completed
      };
    },
    { total: 0, completed: 0 }
  );
}

export function summarizeInstance(instance: TodoInstance): InstanceSummary {
  const counts = countTodos(instance.todos);
  return {
    id: instance.id,
    name: instance.name,
    filePath: instance.filePath,
    historyPath: instance.historyPath,
    createdAt: instance.createdAt,
    updatedAt: instance.updatedAt,
    todoCount: counts.total,
    completedCount: counts.completed
  };
}

export function cloneInstance(instance: TodoInstance): TodoInstance {
  return {
    ...instance,
    todos: instance.todos.map(cloneTodo)
  };
}

export function applyInstanceAction(instance: TodoInstance, action: TodoAction, now: string): TodoInstance {
  switch (action.type) {
    case "addTodo": {
      const todo = createTodo(action.title, now);
      if (!action.parentId) {
        return {
          ...instance,
          updatedAt: now,
          todos: [...instance.todos, todo]
        };
      }

      const appended = appendNode(instance.todos, action.parentId, todo);
      if (!appended.found) {
        throw new Error(`Todo not found: ${action.parentId}`);
      }

      return {
        ...instance,
        updatedAt: now,
        todos: appended.nodes
      };
    }
    case "updateTodoTitle": {
      const updated = updateNodeList(instance.todos, action.todoId, (todo) => ({
        ...todo,
        title: action.title,
        updatedAt: now
      }));
      if (!updated.found) {
        throw new Error(`Todo not found: ${action.todoId}`);
      }

      return {
        ...instance,
        updatedAt: now,
        todos: updated.nodes
      };
    }
    case "toggleTodoCompleted": {
      const updated = updateNodeList(instance.todos, action.todoId, (todo) => ({
        ...todo,
        completed: !todo.completed,
        updatedAt: now
      }));
      if (!updated.found) {
        throw new Error(`Todo not found: ${action.todoId}`);
      }

      return {
        ...instance,
        updatedAt: now,
        todos: updated.nodes
      };
    }
    case "toggleTodoExpanded": {
      const updated = updateNodeList(instance.todos, action.todoId, (todo) => ({
        ...todo,
        expanded: !todo.expanded,
        updatedAt: now
      }));
      if (!updated.found) {
        throw new Error(`Todo not found: ${action.todoId}`);
      }

      return {
        ...instance,
        updatedAt: now,
        todos: updated.nodes
      };
    }
    case "setAllTodosExpanded":
      return {
        ...instance,
        updatedAt: now,
        todos: setExpandedState(instance.todos, action.expanded, now)
      };
    case "setTodoReminder": {
      const updated = updateNodeList(instance.todos, action.todoId, (todo) => ({
        ...todo,
        reminderAt: action.reminderAt,
        updatedAt: now
      }));
      if (!updated.found) {
        throw new Error(`Todo not found: ${action.todoId}`);
      }

      return {
        ...instance,
        updatedAt: now,
        todos: updated.nodes
      };
    }
    case "moveTodo": {
      const removed = removeNodeWithLocation(instance.todos, action.todoId);
      if (!removed.removed) {
        throw new Error(`Todo not found: ${action.todoId}`);
      }

      const targetParentId = action.targetParentId ?? null;
      if (targetParentId && (targetParentId === action.todoId || containsTodo(removed.removed.children, targetParentId))) {
        throw new Error(`Cannot move todo ${action.todoId} into its own descendant`);
      }

      const nextTargetIndex =
        removed.parentId === targetParentId && removed.index >= 0 && removed.index < action.targetIndex ? action.targetIndex - 1 : action.targetIndex;
      const inserted = insertNode(removed.nodes, targetParentId, nextTargetIndex, removed.removed, now);
      if (!inserted.inserted) {
        throw new Error(`Target parent not found: ${targetParentId}`);
      }

      return {
        ...instance,
        updatedAt: now,
        todos: inserted.nodes
      };
    }
    case "deleteTodo": {
      const updated = removeNode(instance.todos, action.todoId);
      if (!updated.found) {
        throw new Error(`Todo not found: ${action.todoId}`);
      }

      return {
        ...instance,
        updatedAt: now,
        todos: updated.nodes
      };
    }
    case "renameInstance":
      return {
        ...instance,
        name: action.name,
        updatedAt: now
      };
    default:
      return instance;
  }
}
