/**
 * SequenceService - Document Sequence Number Generation
 *
 * Provides centralized sequence number generation with support for:
 * - Prefix/suffix patterns
 * - Zero-padding
 * - Periodic reset (yearly/monthly)
 * - Thread-safe increment with database locking
 *
 * @module Core
 */

import { Repository, DataSource } from 'typeorm';
import { Sequence, ResetPeriod } from '../entities/sequence.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 CreateSequenceDto {
  code: string;
  name: string;
  prefix?: string;
  suffix?: string;
  nextNumber?: number;
  padding?: number;
  resetPeriod?: ResetPeriod;
  companyId?: string;
}

export interface UpdateSequenceDto {
  name?: string;
  prefix?: string;
  suffix?: string;
  nextNumber?: number;
  padding?: number;
  resetPeriod?: ResetPeriod;
  isActive?: boolean;
}

export interface SequenceFilters {
  code?: string;
  isActive?: boolean;
  companyId?: string;
  search?: string;
}

export class SequenceService {
  private repository: Repository<Sequence>;
  private dataSource: DataSource;

  constructor(repository: Repository<Sequence>, dataSource: DataSource) {
    this.repository = repository;
    this.dataSource = dataSource;
  }

  /**
   * Create a new sequence
   */
  async create(ctx: ServiceContext, data: CreateSequenceDto): Promise<Sequence> {
    const existing = await this.findByCode(ctx, data.code);
    if (existing) {
      throw new Error(`Sequence with code '${data.code}' already exists`);
    }

    const entity = this.repository.create({
      tenantId: ctx.tenantId,
      code: data.code,
      name: data.name,
      prefix: data.prefix ?? null,
      suffix: data.suffix ?? null,
      nextNumber: data.nextNumber ?? 1,
      padding: data.padding ?? 4,
      resetPeriod: data.resetPeriod ?? ResetPeriod.NONE,
      companyId: data.companyId ?? null,
      createdBy: ctx.userId ?? null,
      isActive: true,
    });

    return this.repository.save(entity);
  }

  /**
   * Find sequence by ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Sequence | null> {
    return this.repository.findOne({
      where: { id, tenantId: ctx.tenantId },
    });
  }

  /**
   * Find sequence by code
   */
  async findByCode(ctx: ServiceContext, code: string): Promise<Sequence | null> {
    return this.repository.findOne({
      where: { tenantId: ctx.tenantId, code },
    });
  }

  /**
   * Get all sequences for tenant
   */
  async findAll(ctx: ServiceContext): Promise<Sequence[]> {
    return this.repository.find({
      where: { tenantId: ctx.tenantId },
      order: { code: 'ASC' },
    });
  }

  /**
   * Find sequences with filters and pagination
   */
  async findWithFilters(
    ctx: ServiceContext,
    filters: SequenceFilters,
    page = 1,
    limit = 50
  ): Promise<PaginatedResult<Sequence>> {
    const qb = this.repository
      .createQueryBuilder('seq')
      .where('seq.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.code) {
      qb.andWhere('seq.code = :code', { code: filters.code });
    }
    if (filters.isActive !== undefined) {
      qb.andWhere('seq.is_active = :isActive', { isActive: filters.isActive });
    }
    if (filters.companyId) {
      qb.andWhere('seq.company_id = :companyId', { companyId: filters.companyId });
    }
    if (filters.search) {
      qb.andWhere('(seq.code ILIKE :search OR seq.name ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

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

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

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

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

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

    return this.repository.save(entity);
  }

  /**
   * Get next sequence number with atomic increment
   * Uses database row-level locking to ensure thread safety
   */
  async getNextNumber(ctx: ServiceContext, code: string): Promise<string> {
    return this.dataSource.transaction(async (manager) => {
      const repository = manager.getRepository(Sequence);

      // Lock the row for update
      const sequence = await repository
        .createQueryBuilder('seq')
        .setLock('pessimistic_write')
        .where('seq.tenant_id = :tenantId', { tenantId: ctx.tenantId })
        .andWhere('seq.code = :code', { code })
        .andWhere('seq.is_active = :isActive', { isActive: true })
        .getOne();

      if (!sequence) {
        throw new Error(`Sequence '${code}' not found or inactive`);
      }

      // Check if reset is needed
      const resetNeeded = this.checkResetNeeded(sequence);
      if (resetNeeded) {
        sequence.nextNumber = 1;
        sequence.lastResetDate = new Date();
      }

      // Build the formatted number
      const formattedNumber = this.formatSequenceNumber(sequence);

      // Increment for next call
      sequence.nextNumber += 1;
      sequence.updatedBy = ctx.userId ?? null;

      await repository.save(sequence);

      return formattedNumber;
    });
  }

  /**
   * Preview next sequence number without incrementing
   */
  async previewNextNumber(ctx: ServiceContext, code: string): Promise<string> {
    const sequence = await this.findByCode(ctx, code);
    if (!sequence || !sequence.isActive) {
      throw new Error(`Sequence '${code}' not found or inactive`);
    }

    // Check if reset would be needed
    const resetNeeded = this.checkResetNeeded(sequence);
    const nextNumber = resetNeeded ? 1 : sequence.nextNumber;

    return this.formatSequenceNumber({
      ...sequence,
      nextNumber,
    });
  }

  /**
   * Reset sequence to specified number
   */
  async resetSequence(
    ctx: ServiceContext,
    code: string,
    startNumber = 1
  ): Promise<Sequence | null> {
    const sequence = await this.findByCode(ctx, code);
    if (!sequence) {
      return null;
    }

    sequence.nextNumber = startNumber;
    sequence.lastResetDate = new Date();
    sequence.updatedBy = ctx.userId ?? null;

    return this.repository.save(sequence);
  }

  /**
   * Activate or deactivate sequence
   */
  async setActive(
    ctx: ServiceContext,
    id: string,
    isActive: boolean
  ): Promise<Sequence | null> {
    return this.update(ctx, id, { isActive });
  }

  /**
   * Delete sequence (hard delete)
   */
  async hardDelete(ctx: ServiceContext, id: string): Promise<boolean> {
    const result = await this.repository.delete({ id, tenantId: ctx.tenantId });
    return (result.affected || 0) > 0;
  }

  /**
   * Check if sequence needs to be reset based on reset period
   */
  private checkResetNeeded(sequence: Sequence): boolean {
    if (!sequence.resetPeriod || sequence.resetPeriod === ResetPeriod.NONE) {
      return false;
    }

    const now = new Date();
    const lastReset = sequence.lastResetDate ? new Date(sequence.lastResetDate) : null;

    if (!lastReset) {
      return false;
    }

    if (sequence.resetPeriod === ResetPeriod.YEAR) {
      return now.getFullYear() !== lastReset.getFullYear();
    }

    if (sequence.resetPeriod === ResetPeriod.MONTH) {
      return (
        now.getFullYear() !== lastReset.getFullYear() ||
        now.getMonth() !== lastReset.getMonth()
      );
    }

    return false;
  }

  /**
   * Format sequence number with prefix, padding, and suffix
   */
  private formatSequenceNumber(sequence: Sequence): string {
    const paddedNumber = String(sequence.nextNumber).padStart(sequence.padding, '0');

    let result = '';

    if (sequence.prefix) {
      result += this.applyDatePatterns(sequence.prefix);
    }

    result += paddedNumber;

    if (sequence.suffix) {
      result += this.applyDatePatterns(sequence.suffix);
    }

    return result;
  }

  /**
   * Apply date patterns to prefix/suffix
   * Supports: {YYYY}, {YY}, {MM}, {DD}
   */
  private applyDatePatterns(template: string): string {
    const now = new Date();
    return template
      .replace(/{YYYY}/g, String(now.getFullYear()))
      .replace(/{YY}/g, String(now.getFullYear()).slice(-2))
      .replace(/{MM}/g, String(now.getMonth() + 1).padStart(2, '0'))
      .replace(/{DD}/g, String(now.getDate()).padStart(2, '0'));
  }
}
