import type { Category, CategoryPatch, CategoryTreeNode, NewCategory } from '../domain/category.js';
import {
  CategoryParentNotContainerError,
  CategoryParentNotFoundError,
  CategoryTreeCycleError,
} from '../domain/errors.js';
import type { CategoryRepository } from '../domain/ports.js';

export class GetCategoryBySlug {
  constructor(private readonly categories: CategoryRepository) {}

  async execute(slug: string): Promise<Category | undefined> {
    return this.categories.findBySlug(slug);
  }
}

export class ListCategoryTree {
  constructor(private readonly categories: CategoryRepository) {}

  async execute(): Promise<CategoryTreeNode[]> {
    return buildTree(await this.categories.list());
  }
}

export class CreateCategory {
  constructor(private readonly categories: CategoryRepository) {}

  async execute(input: NewCategory): Promise<Category> {
    await this.assertValidParent(input.parentId);
    return this.categories.create(input);
  }

  private async assertValidParent(parentId: string | null | undefined): Promise<void> {
    if (parentId === undefined || parentId === null) {
      return;
    }
    const parent = await this.categories.findById(parentId);
    if (!parent) {
      throw new CategoryParentNotFoundError();
    }
    if (!parent.isParent) {
      throw new CategoryParentNotContainerError();
    }
  }
}

export class UpdateCategory {
  constructor(private readonly categories: CategoryRepository) {}

  async execute(id: string, patch: CategoryPatch): Promise<Category | undefined> {
    if (patch.parentId !== undefined) {
      await this.assertValidParent(id, patch.parentId);
    }
    return this.categories.update(id, patch);
  }

  private async assertValidParent(id: string, parentId: string | null): Promise<void> {
    if (parentId === null) {
      return;
    }
    if (parentId === id) {
      throw new CategoryTreeCycleError();
    }
    const parent = await this.categories.findById(parentId);
    if (!parent) {
      throw new CategoryParentNotFoundError();
    }
    const parentIsDescendant = await this.categories.isDescendant(id, parentId);
    if (parentIsDescendant) {
      throw new CategoryTreeCycleError();
    }
    if (!parent.isParent) {
      throw new CategoryParentNotContainerError();
    }
  }
}

export type DeleteCategoryResult = 'deleted' | 'not_found';

export class DeleteCategory {
  constructor(private readonly categories: CategoryRepository) {}

  /**
   * Deletes the category and lets the database cascade to children and
   * product-category links (F-120). Returns:
   *  - 'not_found' if the id does not exist;
   *  - 'deleted' on success.
   */
  async execute(id: string): Promise<DeleteCategoryResult> {
    const category = await this.categories.findById(id);
    if (!category) {
      return 'not_found';
    }
    return (await this.categories.delete(id)) ? 'deleted' : 'not_found';
  }
}

function buildTree(categories: Category[]): CategoryTreeNode[] {
  const nodes = new Map<string, CategoryTreeNode>();
  for (const category of categories) {
    nodes.set(category.id, { ...category, children: [] });
  }

  const roots: CategoryTreeNode[] = [];
  for (const node of nodes.values()) {
    if (node.parentId === null) {
      roots.push(node);
      continue;
    }
    const parent = nodes.get(node.parentId);
    if (parent) {
      parent.children.push(node);
    } else {
      roots.push(node);
    }
  }
  return roots;
}
