/**
 * ProductCategoryService - Product Category Management
 *
 * Provides centralized product category operations:
 * - Hierarchical category tree management
 * - Category path calculations
 * - Parent/child relationships
 *
 * @module Core
 */

import { Repository, IsNull } from 'typeorm';
import { ProductCategory } from '../entities/product-category.entity';

/**
 * Service context for multi-tenant operations
 */
export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

/**
 * Paginated result interface
 */
export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export interface CreateProductCategoryDto {
  name: string;
  code?: string;
  parentId?: string;
  notes?: string;
  active?: boolean;
}

export interface UpdateProductCategoryDto {
  name?: string;
  code?: string;
  parentId?: string;
  notes?: string;
  active?: boolean;
}

export interface ProductCategoryFilters {
  parentId?: string | null;
  active?: boolean;
  search?: string;
}

export interface CategoryTreeNode extends ProductCategory {
  children: CategoryTreeNode[];
  level: number;
}

export class ProductCategoryService {
  private repository: Repository<ProductCategory>;

  constructor(repository: Repository<ProductCategory>) {
    this.repository = repository;
  }

  /**
   * Create a new product category
   */
  async create(
    ctx: ServiceContext,
    data: CreateProductCategoryDto
  ): Promise<ProductCategory> {
    // Check for duplicate code
    if (data.code) {
      const existing = await this.findByCode(ctx, data.code);
      if (existing) {
        throw new Error(`Category with code '${data.code}' already exists`);
      }
    }

    // Validate parent exists
    let fullPath = data.name;
    if (data.parentId) {
      const parent = await this.findById(ctx, data.parentId);
      if (!parent) {
        throw new Error('Parent category not found');
      }
      fullPath = parent.fullPath ? `${parent.fullPath} / ${data.name}` : data.name;
    }

    const entity = this.repository.create({
      tenantId: ctx.tenantId,
      name: data.name,
      code: data.code ?? null,
      parentId: data.parentId ?? null,
      fullPath,
      notes: data.notes ?? null,
      active: data.active ?? true,
      createdBy: ctx.userId ?? null,
    });

    return this.repository.save(entity);
  }

  /**
   * Find category by ID
   */
  async findById(
    ctx: ServiceContext,
    id: string
  ): Promise<ProductCategory | null> {
    return this.repository.findOne({
      where: { id, tenantId: ctx.tenantId, deletedAt: IsNull() },
      relations: ['parent', 'children'],
    });
  }

  /**
   * Find category by code
   */
  async findByCode(
    ctx: ServiceContext,
    code: string
  ): Promise<ProductCategory | null> {
    return this.repository.findOne({
      where: { tenantId: ctx.tenantId, code, deletedAt: IsNull() },
      relations: ['parent', 'children'],
    });
  }

  /**
   * Get all root categories (no parent)
   */
  async findRootCategories(ctx: ServiceContext): Promise<ProductCategory[]> {
    return this.repository.find({
      where: {
        tenantId: ctx.tenantId,
        parentId: IsNull(),
        active: true,
        deletedAt: IsNull(),
      },
      relations: ['children'],
      order: { name: 'ASC' },
    });
  }

  /**
   * Get all active categories (flat list)
   */
  async findAll(ctx: ServiceContext): Promise<ProductCategory[]> {
    return this.repository.find({
      where: { tenantId: ctx.tenantId, active: true, deletedAt: IsNull() },
      relations: ['parent'],
      order: { fullPath: 'ASC' },
    });
  }

  /**
   * Find categories with filters and pagination
   */
  async findWithFilters(
    ctx: ServiceContext,
    filters: ProductCategoryFilters,
    page = 1,
    limit = 50
  ): Promise<PaginatedResult<ProductCategory>> {
    const qb = this.repository
      .createQueryBuilder('cat')
      .leftJoinAndSelect('cat.parent', 'parent')
      .where('cat.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('cat.deleted_at IS NULL');

    if (filters.parentId !== undefined) {
      if (filters.parentId === null) {
        qb.andWhere('cat.parent_id IS NULL');
      } else {
        qb.andWhere('cat.parent_id = :parentId', { parentId: filters.parentId });
      }
    }
    if (filters.active !== undefined) {
      qb.andWhere('cat.active = :active', { active: filters.active });
    }
    if (filters.search) {
      qb.andWhere('(cat.name ILIKE :search OR cat.code ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

    const skip = (page - 1) * limit;
    qb.orderBy('cat.full_path', 'ASC').skip(skip).take(limit);

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  /**
   * Get children of a category
   */
  async findChildren(
    ctx: ServiceContext,
    parentId: string
  ): Promise<ProductCategory[]> {
    return this.repository.find({
      where: {
        tenantId: ctx.tenantId,
        parentId,
        active: true,
        deletedAt: IsNull(),
      },
      order: { name: 'ASC' },
    });
  }

  /**
   * Get category tree (hierarchical structure)
   */
  async getCategoryTree(ctx: ServiceContext): Promise<CategoryTreeNode[]> {
    const allCategories = await this.repository.find({
      where: { tenantId: ctx.tenantId, active: true, deletedAt: IsNull() },
      order: { name: 'ASC' },
    });

    // Build tree from flat list
    const categoryMap = new Map<string, CategoryTreeNode>();
    const roots: CategoryTreeNode[] = [];

    // First pass: create all nodes
    for (const cat of allCategories) {
      categoryMap.set(cat.id, {
        ...cat,
        children: [],
        level: 0,
      });
    }

    // Second pass: build tree structure
    for (const cat of allCategories) {
      const node = categoryMap.get(cat.id)!;
      if (cat.parentId && categoryMap.has(cat.parentId)) {
        const parent = categoryMap.get(cat.parentId)!;
        parent.children.push(node);
        node.level = parent.level + 1;
      } else {
        roots.push(node);
      }
    }

    return roots;
  }

  /**
   * Get flattened tree (for dropdowns, with indentation level)
   */
  async getFlattenedTree(ctx: ServiceContext): Promise<CategoryTreeNode[]> {
    const tree = await this.getCategoryTree(ctx);
    const result: CategoryTreeNode[] = [];

    const flatten = (nodes: CategoryTreeNode[]) => {
      for (const node of nodes) {
        result.push(node);
        if (node.children.length > 0) {
          flatten(node.children);
        }
      }
    };

    flatten(tree);
    return result;
  }

  /**
   * Update category
   */
  async update(
    ctx: ServiceContext,
    id: string,
    data: UpdateProductCategoryDto
  ): Promise<ProductCategory | null> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return null;
    }

    // Check for code conflicts
    if (data.code && data.code !== entity.code) {
      const existing = await this.findByCode(ctx, data.code);
      if (existing && existing.id !== id) {
        throw new Error(`Category with code '${data.code}' already exists`);
      }
    }

    // Validate parent
    if (data.parentId !== undefined) {
      if (data.parentId === id) {
        throw new Error('Category cannot be its own parent');
      }
      if (data.parentId) {
        const parent = await this.findById(ctx, data.parentId);
        if (!parent) {
          throw new Error('Parent category not found');
        }
        // Check for circular reference
        if (await this.isDescendant(ctx, data.parentId, id)) {
          throw new Error('Circular reference detected');
        }
      }
    }

    // Update entity
    Object.assign(entity, {
      ...data,
      updatedBy: ctx.userId ?? null,
    });

    // Recalculate full path if name or parent changed
    if (data.name !== undefined || data.parentId !== undefined) {
      entity.fullPath = await this.calculateFullPath(ctx, entity);
    }

    const saved = await this.repository.save(entity);

    // Update children paths if needed
    if (data.name !== undefined || data.parentId !== undefined) {
      await this.updateChildrenPaths(ctx, id);
    }

    return saved;
  }

  /**
   * Soft delete category
   */
  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return false;
    }

    // Check for children
    const children = await this.findChildren(ctx, id);
    if (children.length > 0) {
      throw new Error('Cannot delete category with children');
    }

    entity.deletedAt = new Date();
    entity.deletedBy = ctx.userId ?? null;
    await this.repository.save(entity);
    return true;
  }

  /**
   * Get ancestors of a category
   */
  async getAncestors(ctx: ServiceContext, id: string): Promise<ProductCategory[]> {
    const ancestors: ProductCategory[] = [];
    let current = await this.findById(ctx, id);

    while (current && current.parentId) {
      const parent = await this.findById(ctx, current.parentId);
      if (parent) {
        ancestors.unshift(parent);
        current = parent;
      } else {
        break;
      }
    }

    return ancestors;
  }

  /**
   * Get all descendants of a category
   */
  async getDescendants(ctx: ServiceContext, id: string): Promise<ProductCategory[]> {
    const descendants: ProductCategory[] = [];

    const collectDescendants = async (parentId: string) => {
      const children = await this.findChildren(ctx, parentId);
      for (const child of children) {
        descendants.push(child);
        await collectDescendants(child.id);
      }
    };

    await collectDescendants(id);
    return descendants;
  }

  /**
   * Check if a category is descendant of another
   */
  private async isDescendant(
    ctx: ServiceContext,
    categoryId: string,
    potentialAncestorId: string
  ): Promise<boolean> {
    const ancestors = await this.getAncestors(ctx, categoryId);
    return ancestors.some((a) => a.id === potentialAncestorId);
  }

  /**
   * Calculate full path for a category
   */
  private async calculateFullPath(
    ctx: ServiceContext,
    category: ProductCategory
  ): Promise<string> {
    if (!category.parentId) {
      return category.name;
    }

    const parent = await this.findById(ctx, category.parentId);
    if (!parent) {
      return category.name;
    }

    return `${parent.fullPath || parent.name} / ${category.name}`;
  }

  /**
   * Update full paths for all children
   */
  private async updateChildrenPaths(
    ctx: ServiceContext,
    parentId: string
  ): Promise<void> {
    const children = await this.findChildren(ctx, parentId);

    for (const child of children) {
      child.fullPath = await this.calculateFullPath(ctx, child);
      await this.repository.save(child);
      await this.updateChildrenPaths(ctx, child.id);
    }
  }
}
