import {
  Injectable,
  ConflictException,
  NotFoundException,
  BadRequestException,
  InternalServerErrorException,
  Logger,
} from '@nestjs/common';
import { PrismaService } from '../common/prisma/prisma.service';
import { CreateSaleDto, CreateSaleItemDto } from './dto/create-sale.dto';
import { SaleResponseDto } from './dto/sale-response.dto';
import { AuthenticatedUser } from '../auth/guards/tenant.guard';
import { 
  Product, 
  Sale, 
  SaleItem, 
  Prisma, 
  AdjustmentType,
  SaleStatus 
} from '@prisma/client';
import { Decimal } from '@prisma/client/runtime/library';

export interface SaleWithItems extends Sale {
  saleItems: SaleItem[];
}

interface ProductWithStock extends Product {
  requestedQuantity: number;
  unitPrice: number;
}

@Injectable()
export class SalesService {
  private readonly logger = new Logger(SalesService.name);

  constructor(private prisma: PrismaService) {}

  /**
   * Creates a new sale with complete transaction handling and inventory management
   * @param createSaleDto Sale data transfer object
   * @param user Authenticated user making the request
   * @returns Created sale with items
   */
  async createSale(
    createSaleDto: CreateSaleDto,
    user: AuthenticatedUser,
  ): Promise<SaleResponseDto> {
    this.logger.log(`Creating sale for tenant ${user.tenantId} by user ${user.id}`);

    try {
      // Validate tenant access
      const tenantId = this.validateTenantAccess(user);

      // Validate sale items
      this.validateSaleItems(createSaleDto.items);

      // Create sale within transaction
      const sale = await this.prisma.$transaction(
        async (tx) => {
          // 1. Fetch and validate products with stock check
          const productsWithStock = await this.fetchAndValidateProducts(
            tx,
            createSaleDto.items,
            tenantId,
          );

          // 2. Calculate totals
          const totals = this.calculateSaleTotals(
            productsWithStock,
            createSaleDto.taxAmount || 0,
            createSaleDto.discountAmount || 0,
          );

          // 3. Generate unique sale number
          const saleNumber = await this.generateSaleNumber(tx, tenantId);

          // 4. Create sale record
          const newSale = await this.createSaleRecord(
            tx,
            {
              tenantId,
              saleNumber,
              ...totals,
              paymentMethod: createSaleDto.paymentMethod,
              customerName: createSaleDto.customerName,
              customerEmail: createSaleDto.customerEmail,
              customerPhone: createSaleDto.customerPhone,
              notes: createSaleDto.notes,
              processedBy: user.id,
            }
          );

          // 5. Create sale items
          const saleItems = await this.createSaleItems(
            tx,
            newSale.id,
            productsWithStock,
          );

          // 6. Update product inventory and create adjustment records
          await this.updateInventoryAndCreateAdjustments(
            tx,
            productsWithStock,
            newSale.id,
            tenantId,
            user.id,
          );

          return {
            ...newSale,
            saleItems,
          };
        },
        {
          isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
          timeout: 10000, // 10 seconds timeout
        },
      );

      this.logger.log(`Sale created successfully: ${sale.saleNumber}`);
      return this.formatSaleResponse(sale);

    } catch (error) {
      this.logger.error(`Failed to create sale: ${error.message}`, error.stack);
      
      // Re-throw known exceptions
      if (
        error instanceof ConflictException ||
        error instanceof NotFoundException ||
        error instanceof BadRequestException
      ) {
        throw error;
      }

      // Handle Prisma errors
      if (error instanceof Prisma.PrismaClientKnownRequestError) {
        return this.handlePrismaError(error);
      }

      throw new InternalServerErrorException('Failed to process sale');
    }
  }

  /**
   * Validates tenant access and returns tenant ID
   */
  private validateTenantAccess(user: AuthenticatedUser): string {
    if (!user.tenantId) {
      throw new BadRequestException('User must belong to a tenant to create sales');
    }

    if (!user.isActive) {
      throw new BadRequestException('User account is inactive');
    }

    return user.tenantId;
  }

  /**
   * Validates sale items basic structure
   */
  private validateSaleItems(items: CreateSaleItemDto[]): void {
    if (!items || items.length === 0) {
      throw new BadRequestException('Sale must contain at least one item');
    }

    // Check for duplicate products
    const productIds = items.map(item => item.productId);
    const uniqueProductIds = new Set(productIds);
    
    if (productIds.length !== uniqueProductIds.size) {
      throw new BadRequestException('Duplicate products in sale items');
    }
  }

  /**
   * Fetches products and validates availability
   */
  private async fetchAndValidateProducts(
    tx: Prisma.TransactionClient,
    items: CreateSaleItemDto[],
    tenantId: string,
  ): Promise<ProductWithStock[]> {
    const productIds = items.map(item => item.productId);

    // Fetch products with row-level locking to prevent race conditions
    const products = await tx.product.findMany({
      where: {
        id: { in: productIds },
        tenantId,
        isActive: true,
        deletedAt: null,
      },
      // Lock rows for update to prevent concurrent modifications
      // Note: Prisma doesn't support FOR UPDATE directly, but transaction isolation helps
    });

    // Check if all products were found
    if (products.length !== productIds.length) {
      const foundIds = products.map(p => p.id);
      const missingIds = productIds.filter(id => !foundIds.includes(id));
      throw new NotFoundException(
        `Products not found or inactive: ${missingIds.join(', ')}`
      );
    }

    // Map products with requested quantities and validate stock
    return products.map(product => {
      const item = items.find(i => i.productId === product.id)!;
      
      // Check stock availability
      if (product.stock < item.quantity) {
        throw new ConflictException(
          `Insufficient stock for product "${product.name}". Available: ${product.stock}, Requested: ${item.quantity}`
        );
      }

      return {
        ...product,
        requestedQuantity: item.quantity,
        unitPrice: item.unitPrice || product.price.toNumber(),
      };
    });
  }

  /**
   * Calculates sale totals
   */
  private calculateSaleTotals(
    products: ProductWithStock[],
    taxAmount: number,
    discountAmount: number,
  ): {
    subtotal: number;
    taxAmount: number;
    discountAmount: number;
    total: number;
  } {
    const subtotal = products.reduce((sum, product) => {
      return sum + (product.unitPrice * product.requestedQuantity);
    }, 0);

    const total = subtotal + taxAmount - discountAmount;

    if (total < 0) {
      throw new BadRequestException('Sale total cannot be negative');
    }

    return {
      subtotal,
      taxAmount,
      discountAmount,
      total,
    };
  }

  /**
   * Generates unique sale number
   */
  private async generateSaleNumber(
    tx: Prisma.TransactionClient,
    tenantId: string,
  ): Promise<string> {
    const today = new Date();
    const datePrefix = today.toISOString().slice(0, 10).replace(/-/g, '');

    // Get today's sale count for this tenant
    const todaySalesCount = await tx.sale.count({
      where: {
        tenantId,
        createdAt: {
          gte: new Date(today.getFullYear(), today.getMonth(), today.getDate()),
          lt: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1),
        },
      },
    });

    const saleNumber = `${datePrefix}-${String(todaySalesCount + 1).padStart(4, '0')}`;
    return saleNumber;
  }

  /**
   * Creates sale record
   */
  private async createSaleRecord(
    tx: Prisma.TransactionClient,
    saleData: {
      tenantId: string;
      saleNumber: string;
      subtotal: number;
      taxAmount: number;
      discountAmount: number;
      total: number;
      paymentMethod: any;
      customerName?: string;
      customerEmail?: string;
      customerPhone?: string;
      notes?: string;
      processedBy: string;
    },
  ): Promise<Sale> {
    return tx.sale.create({
      data: {
        tenantId: saleData.tenantId,
        saleNumber: saleData.saleNumber,
        subtotal: new Decimal(saleData.subtotal),
        taxAmount: new Decimal(saleData.taxAmount),
        discountAmount: new Decimal(saleData.discountAmount),
        total: new Decimal(saleData.total),
        paymentMethod: saleData.paymentMethod,
        customerName: saleData.customerName,
        customerEmail: saleData.customerEmail,
        customerPhone: saleData.customerPhone,
        notes: saleData.notes,
        processedBy: saleData.processedBy,
        status: SaleStatus.COMPLETED,
      },
    });
  }

  /**
   * Creates sale items
   */
  private async createSaleItems(
    tx: Prisma.TransactionClient,
    saleId: string,
    products: ProductWithStock[],
  ): Promise<SaleItem[]> {
    const saleItemsData = products.map(product => ({
      saleId,
      productId: product.id,
      productName: product.name,
      productSku: product.sku,
      quantity: product.requestedQuantity,
      unitPrice: new Decimal(product.unitPrice),
      totalPrice: new Decimal(product.unitPrice * product.requestedQuantity),
    }));

    // Create all sale items in batch
    await tx.saleItem.createMany({
      data: saleItemsData,
    });

    // Return created items
    return tx.saleItem.findMany({
      where: { saleId },
    });
  }

  /**
   * Updates inventory and creates adjustment records
   */
  private async updateInventoryAndCreateAdjustments(
    tx: Prisma.TransactionClient,
    products: ProductWithStock[],
    saleId: string,
    tenantId: string,
    userId: string,
  ): Promise<void> {
    for (const product of products) {
      const previousStock = product.stock;
      const newStock = previousStock - product.requestedQuantity;

      // Update product stock
      await tx.product.update({
        where: { id: product.id },
        data: { stock: newStock },
      });

      // Create inventory adjustment record
      await tx.inventoryAdjustment.create({
        data: {
          tenantId,
          productId: product.id,
          adjustmentType: AdjustmentType.SALE,
          quantityChange: -product.requestedQuantity,
          previousStock,
          newStock,
          reason: 'Sale transaction',
          referenceId: saleId,
          adjustedBy: userId,
        },
      });
    }
  }

  /**
   * Formats sale response
   */
  private formatSaleResponse(sale: SaleWithItems): SaleResponseDto {
    return {
      id: sale.id,
      tenantId: sale.tenantId,
      saleNumber: sale.saleNumber,
      customerName: sale.customerName,
      customerEmail: sale.customerEmail,
      customerPhone: sale.customerPhone,
      subtotal: sale.subtotal.toNumber(),
      taxAmount: sale.taxAmount.toNumber(),
      discountAmount: sale.discountAmount.toNumber(),
      total: sale.total.toNumber(),
      paymentMethod: sale.paymentMethod,
      status: sale.status,
      notes: sale.notes,
      processedBy: sale.processedBy,
      createdAt: sale.createdAt,
      updatedAt: sale.updatedAt,
      saleItems: sale.saleItems.map(item => ({
        id: item.id,
        productId: item.productId,
        productName: item.productName,
        productSku: item.productSku,
        quantity: item.quantity,
        unitPrice: item.unitPrice.toNumber(),
        totalPrice: item.totalPrice.toNumber(),
        createdAt: item.createdAt,
      })),
    };
  }

  /**
   * Handles Prisma specific errors
   */
  private handlePrismaError(error: Prisma.PrismaClientKnownRequestError): never {
    switch (error.code) {
      case 'P2002':
        throw new ConflictException('Sale number already exists');
      case 'P2025':
        throw new NotFoundException('Record not found');
      default:
        this.logger.error(`Unhandled Prisma error: ${error.code}`, error);
        throw new InternalServerErrorException('Database operation failed');
    }
  }

  /**
   * Get sales by tenant with pagination and filtering
   */
  async findAll(
    user: AuthenticatedUser,
    page: number = 1,
    limit: number = 20,
    status?: SaleStatus,
    startDate?: Date,
    endDate?: Date,
  ): Promise<{ sales: SaleResponseDto[]; total: number }> {
    const tenantId = this.validateTenantAccess(user);
    const skip = (page - 1) * limit;

    const where: Prisma.SaleWhereInput = {
      tenantId,
      ...(status && { status }),
      ...(startDate || endDate ? {
        createdAt: {
          ...(startDate && { gte: startDate }),
          ...(endDate && { lte: endDate }),
        },
      } : {}),
    };

    const [sales, total] = await Promise.all([
      this.prisma.sale.findMany({
        where,
        include: { saleItems: true },
        orderBy: { createdAt: 'desc' },
        skip,
        take: limit,
      }),
      this.prisma.sale.count({ where }),
    ]);

    return {
      sales: sales.map(sale => this.formatSaleResponse(sale)),
      total,
    };
  }

  /**
   * Get single sale by ID
   */
  async findOne(id: string, user: AuthenticatedUser): Promise<SaleResponseDto> {
    const tenantId = this.validateTenantAccess(user);

    const sale = await this.prisma.sale.findFirst({
      where: { id, tenantId },
      include: { saleItems: true },
    });

    if (!sale) {
      throw new NotFoundException('Sale not found');
    }

    return this.formatSaleResponse(sale);
  }
}