/**
 * UomService - Unit of Measure Management
 *
 * Provides centralized UoM operations:
 * - UoM category management
 * - Unit of measure CRUD
 * - Unit conversion calculations
 *
 * @module Core
 */

import { Repository } from 'typeorm';
import { Uom, UomType } from '../entities/uom.entity';
import { UomCategory } from '../entities/uom-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 CreateUomCategoryDto {
  name: string;
  description?: string;
}

export interface UpdateUomCategoryDto {
  name?: string;
  description?: string;
}

export interface CreateUomDto {
  categoryId: string;
  name: string;
  code?: string;
  uomType?: UomType;
  factor?: number;
  rounding?: number;
  active?: boolean;
}

export interface UpdateUomDto {
  name?: string;
  code?: string;
  uomType?: UomType;
  factor?: number;
  rounding?: number;
  active?: boolean;
}

export interface UomFilters {
  categoryId?: string;
  active?: boolean;
  search?: string;
}

export interface ConversionResult {
  fromUom: string;
  toUom: string;
  originalQuantity: number;
  convertedQuantity: number;
  factor: number;
}

export class UomService {
  private categoryRepository: Repository<UomCategory>;
  private uomRepository: Repository<Uom>;

  constructor(
    categoryRepository: Repository<UomCategory>,
    uomRepository: Repository<Uom>
  ) {
    this.categoryRepository = categoryRepository;
    this.uomRepository = uomRepository;
  }

  // ==================== CATEGORY OPERATIONS ====================

  /**
   * Create a new UoM category
   */
  async createCategory(
    ctx: ServiceContext,
    data: CreateUomCategoryDto
  ): Promise<UomCategory> {
    const existing = await this.categoryRepository.findOne({
      where: { tenantId: ctx.tenantId, name: data.name },
    });

    if (existing) {
      throw new Error(`UoM category '${data.name}' already exists`);
    }

    const entity = this.categoryRepository.create({
      tenantId: ctx.tenantId,
      name: data.name,
      description: data.description ?? null,
    });

    return this.categoryRepository.save(entity);
  }

  /**
   * Find category by ID
   */
  async findCategoryById(
    ctx: ServiceContext,
    id: string
  ): Promise<UomCategory | null> {
    return this.categoryRepository.findOne({
      where: { id, tenantId: ctx.tenantId },
      relations: ['uoms'],
    });
  }

  /**
   * Get all categories for tenant
   */
  async findAllCategories(ctx: ServiceContext): Promise<UomCategory[]> {
    return this.categoryRepository.find({
      where: { tenantId: ctx.tenantId },
      relations: ['uoms'],
      order: { name: 'ASC' },
    });
  }

  /**
   * Update category
   */
  async updateCategory(
    ctx: ServiceContext,
    id: string,
    data: UpdateUomCategoryDto
  ): Promise<UomCategory | null> {
    const entity = await this.findCategoryById(ctx, id);
    if (!entity) {
      return null;
    }

    if (data.name && data.name !== entity.name) {
      const existing = await this.categoryRepository.findOne({
        where: { tenantId: ctx.tenantId, name: data.name },
      });
      if (existing) {
        throw new Error(`UoM category '${data.name}' already exists`);
      }
    }

    Object.assign(entity, data);
    return this.categoryRepository.save(entity);
  }

  /**
   * Delete category (only if no UoMs assigned)
   */
  async deleteCategory(ctx: ServiceContext, id: string): Promise<boolean> {
    const category = await this.findCategoryById(ctx, id);
    if (!category) {
      return false;
    }

    // Check for associated UoMs
    const uomCount = await this.uomRepository.count({
      where: { tenantId: ctx.tenantId, categoryId: id },
    });

    if (uomCount > 0) {
      throw new Error('Cannot delete category with associated units of measure');
    }

    const result = await this.categoryRepository.delete({
      id,
      tenantId: ctx.tenantId,
    });
    return (result.affected || 0) > 0;
  }

  // ==================== UOM OPERATIONS ====================

  /**
   * Create a new unit of measure
   */
  async create(ctx: ServiceContext, data: CreateUomDto): Promise<Uom> {
    // Validate category exists
    const category = await this.findCategoryById(ctx, data.categoryId);
    if (!category) {
      throw new Error('UoM category not found');
    }

    // Check for duplicate name in category
    const existing = await this.uomRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        categoryId: data.categoryId,
        name: data.name,
      },
    });

    if (existing) {
      throw new Error(`UoM '${data.name}' already exists in this category`);
    }

    // If this is the first UoM in category, it must be reference type
    const categoryUoms = await this.findByCategory(ctx, data.categoryId);
    if (categoryUoms.length === 0 && data.uomType !== UomType.REFERENCE) {
      throw new Error('First unit in category must be reference type');
    }

    // Reference type must have factor of 1
    if (data.uomType === UomType.REFERENCE && data.factor !== 1) {
      throw new Error('Reference unit must have factor of 1');
    }

    const entity = this.uomRepository.create({
      tenantId: ctx.tenantId,
      categoryId: data.categoryId,
      name: data.name,
      code: data.code ?? null,
      uomType: data.uomType ?? UomType.REFERENCE,
      factor: data.factor ?? 1,
      rounding: data.rounding ?? 0.01,
      active: data.active ?? true,
    });

    return this.uomRepository.save(entity);
  }

  /**
   * Find UoM by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Uom | null> {
    return this.uomRepository.findOne({
      where: { id, tenantId: ctx.tenantId },
      relations: ['category'],
    });
  }

  /**
   * Find UoM by code
   */
  async findByCode(ctx: ServiceContext, code: string): Promise<Uom | null> {
    return this.uomRepository.findOne({
      where: { tenantId: ctx.tenantId, code },
      relations: ['category'],
    });
  }

  /**
   * Get all UoMs by category
   */
  async findByCategory(ctx: ServiceContext, categoryId: string): Promise<Uom[]> {
    return this.uomRepository.find({
      where: { tenantId: ctx.tenantId, categoryId },
      order: { name: 'ASC' },
    });
  }

  /**
   * Get all active UoMs for tenant
   */
  async findAll(ctx: ServiceContext): Promise<Uom[]> {
    return this.uomRepository.find({
      where: { tenantId: ctx.tenantId, active: true },
      relations: ['category'],
      order: { name: 'ASC' },
    });
  }

  /**
   * Find UoMs with filters and pagination
   */
  async findWithFilters(
    ctx: ServiceContext,
    filters: UomFilters,
    page = 1,
    limit = 50
  ): Promise<PaginatedResult<Uom>> {
    const qb = this.uomRepository
      .createQueryBuilder('uom')
      .leftJoinAndSelect('uom.category', 'category')
      .where('uom.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.categoryId) {
      qb.andWhere('uom.category_id = :categoryId', {
        categoryId: filters.categoryId,
      });
    }
    if (filters.active !== undefined) {
      qb.andWhere('uom.active = :active', { active: filters.active });
    }
    if (filters.search) {
      qb.andWhere('(uom.name ILIKE :search OR uom.code ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

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

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

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

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

    // Cannot change reference type factor
    if (entity.uomType === UomType.REFERENCE && data.factor && data.factor !== 1) {
      throw new Error('Cannot change factor for reference unit');
    }

    Object.assign(entity, data);
    return this.uomRepository.save(entity);
  }

  /**
   * Delete UoM (soft delete by setting inactive)
   */
  async delete(ctx: ServiceContext, id: string): Promise<boolean> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return false;
    }

    // Cannot delete reference unit if others exist
    if (entity.uomType === UomType.REFERENCE) {
      const otherUoms = await this.uomRepository.count({
        where: {
          tenantId: ctx.tenantId,
          categoryId: entity.categoryId,
          active: true,
        },
      });
      if (otherUoms > 1) {
        throw new Error('Cannot delete reference unit while other units exist');
      }
    }

    entity.active = false;
    await this.uomRepository.save(entity);
    return true;
  }

  // ==================== CONVERSION OPERATIONS ====================

  /**
   * Get reference unit for a category
   */
  async getReferenceUnit(
    ctx: ServiceContext,
    categoryId: string
  ): Promise<Uom | null> {
    return this.uomRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        categoryId,
        uomType: UomType.REFERENCE,
        active: true,
      },
    });
  }

  /**
   * Convert quantity between UoMs (must be same category)
   */
  async convert(
    ctx: ServiceContext,
    quantity: number,
    fromUomId: string,
    toUomId: string
  ): Promise<ConversionResult> {
    // Same UoM - no conversion
    if (fromUomId === toUomId) {
      const uom = await this.findById(ctx, fromUomId);
      return {
        fromUom: uom?.name || fromUomId,
        toUom: uom?.name || toUomId,
        originalQuantity: quantity,
        convertedQuantity: quantity,
        factor: 1,
      };
    }

    const fromUom = await this.findById(ctx, fromUomId);
    const toUom = await this.findById(ctx, toUomId);

    if (!fromUom || !toUom) {
      throw new Error('Unit of measure not found');
    }

    if (fromUom.categoryId !== toUom.categoryId) {
      throw new Error('Cannot convert between different UoM categories');
    }

    // Convert through reference unit
    // fromUom -> reference -> toUom
    const referenceQuantity = quantity * Number(fromUom.factor);
    const convertedQuantity = referenceQuantity / Number(toUom.factor);

    // Apply rounding
    const rounding = Number(toUom.rounding) || 0.01;
    const roundedQuantity = Math.round(convertedQuantity / rounding) * rounding;

    return {
      fromUom: fromUom.name,
      toUom: toUom.name,
      originalQuantity: quantity,
      convertedQuantity: roundedQuantity,
      factor: Number(fromUom.factor) / Number(toUom.factor),
    };
  }

  /**
   * Get conversion factor between two UoMs
   */
  async getConversionFactor(
    ctx: ServiceContext,
    fromUomId: string,
    toUomId: string
  ): Promise<number> {
    if (fromUomId === toUomId) {
      return 1;
    }

    const fromUom = await this.findById(ctx, fromUomId);
    const toUom = await this.findById(ctx, toUomId);

    if (!fromUom || !toUom) {
      throw new Error('Unit of measure not found');
    }

    if (fromUom.categoryId !== toUom.categoryId) {
      throw new Error('Cannot get factor between different UoM categories');
    }

    return Number(fromUom.factor) / Number(toUom.factor);
  }
}
