import { Injectable, Logger } from '@nestjs/common';
import { BusinessException } from '../../../common/exceptions';
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
import { Repository, EntityManager, IsNull, Equal } from 'typeorm';
import { ConfigService } from '@nestjs/config';

import { DocumentNumberFormat } from '../entities/document-number-format.entity';
import { DocumentNumberAudit } from '../entities/document-number-audit.entity';
import { DocumentNumberError } from '../entities/document-number-error.entity';

// Services
import { CounterService } from './counter.service';
import { ReservationService } from './reservation.service';
import { FormatService } from './format.service';
import { DocumentNumberingLockService } from './document-numbering-lock.service';
import { ManualOverrideService } from './manual-override.service';
import { MetricsService } from './metrics.service';

// DTOs
import { CounterKeyDto } from '../dto/counter-key.dto';
import { GenerateNumberContext } from '../interfaces/document-numbering.interface';
import {
  ReserveNumberDto,
  ReserveNumberResponseDto,
} from '../dto/reserve-number.dto';
import {
  ConfirmReservationDto,
  ConfirmReservationResponseDto,
} from '../dto/confirm-reservation.dto';
import { ManualOverrideDto } from '../dto/manual-override.dto';
import { UuidResolverService } from '../../../common/services/uuid-resolver.service';

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

  constructor(
    @InjectRepository(DocumentNumberFormat)
    private formatRepo: Repository<DocumentNumberFormat>,
    @InjectRepository(DocumentNumberAudit)
    private auditRepo: Repository<DocumentNumberAudit>,
    @InjectRepository(DocumentNumberError)
    private errorRepo: Repository<DocumentNumberError>,

    private counterService: CounterService,
    private reservationService: ReservationService,
    private formatService: FormatService,
    private lockService: DocumentNumberingLockService,
    private configService: ConfigService,
    private manualOverrideService: ManualOverrideService,
    private metricsService: MetricsService,
    @InjectEntityManager()
    private entityManager: EntityManager,
    private uuidResolver: UuidResolverService
  ) {}

  /**
   * ADR-019: Public facade for controllers to resolve project/organization IDs
   */
  async resolveIdForPreview(
    type: 'project' | 'organization',
    id: number | string
  ): Promise<number> {
    if (type === 'project') return this.uuidResolver.resolveProjectId(id);
    return this.uuidResolver.resolveOrganizationId(id);
  }

  async generateNextNumber(
    ctx: GenerateNumberContext
  ): Promise<{ number: string; auditId: number }> {
    let lock = null;
    try {
      // 0. Check Idempotency (Ideally done in Guard/Middleware, but double check here if passed)
      // Note: If idempotencyKey exists in ctx, check audit log for existing SUCCESS entry?
      // Omitted for brevity as per spec usually handled by middleware or separate check.

      const currentYear = new Date().getFullYear();
      const resetScope = `YEAR_${currentYear}`;

      // 1. Prepare Counter Key
      const key: CounterKeyDto = {
        projectId: ctx.projectId,
        originatorOrganizationId: ctx.originatorOrganizationId,
        recipientOrganizationId: ctx.recipientOrganizationId || 0,
        correspondenceTypeId: ctx.typeId,
        subTypeId: ctx.subTypeId || 0,
        rfaTypeId: ctx.rfaTypeId || 0,
        disciplineId: ctx.disciplineId || 0,
        resetScope: resetScope,
      };

      // 2. Acquire Redis Lock
      try {
        // Map CounterKeyDto to LockCounterKey (names slightly different or cast if same)
        lock = await this.lockService.acquireLock({
          projectId: key.projectId,
          originatorOrgId: key.originatorOrganizationId,
          recipientOrgId: key.recipientOrganizationId,
          correspondenceTypeId: key.correspondenceTypeId,
          subTypeId: key.subTypeId,
          rfaTypeId: key.rfaTypeId,
          disciplineId: key.disciplineId,
          resetScope: key.resetScope,
        });
      } catch (e) {
        const errorMessage = e instanceof Error ? e.message : String(e);
        this.logger.warn(
          `Failed to acquire Redis lock, falling back to DB lock only: ${errorMessage}`
        );
        this.metricsService.lockFailures.inc({
          project_id: String(key.projectId),
        });
        // Fallback: Proceed without Redlock, relying on CounterService DB optimistic lock
      }

      // 3. Increment Counter
      const sequence = await this.counterService.incrementCounter(key);

      // 4. Format Number
      const { previewNumber: documentNumber } = await this.formatService.format(
        {
          projectId: ctx.projectId,
          correspondenceTypeId: ctx.typeId,
          subTypeId: ctx.subTypeId,
          rfaTypeId: ctx.rfaTypeId,
          disciplineId: ctx.disciplineId,
          sequence: sequence,
          resetScope: resetScope,
          year: currentYear,
          originatorOrganizationId: ctx.originatorOrganizationId,
          recipientOrganizationId: ctx.recipientOrganizationId,
        }
      );

      // 5. Audit Log
      const audit = await this.logAudit({
        documentNumber,
        counterKey: JSON.stringify(key),
        templateUsed: 'DELEGATED_TO_FORMAT_SERVICE',
        context: ctx,
        isSuccess: true,
        operation: 'GENERATE',
        // metadata: { idempotencyKey: ctx.idempotencyKey } // If available
      });

      this.metricsService.numbersGenerated.inc({
        project_id: String(ctx.projectId),
        type_id: String(ctx.typeId),
      });

      return { number: documentNumber, auditId: audit.id };
    } catch (error: unknown) {
      await this.logError(error, ctx, 'GENERATE');
      throw error;
    } finally {
      if (lock) {
        await this.lockService.releaseLock(lock);
      }
    }
  }

  async reserveNumber(
    dto: ReserveNumberDto,
    userId: number,
    ipAddress?: string
  ): Promise<ReserveNumberResponseDto> {
    try {
      // Delegate completely to ReservationService
      return await this.reservationService.reserve(
        dto,
        userId,
        ipAddress || '0.0.0.0',
        'Unknown' // userAgent not passed in legacy call
      );
    } catch (error: unknown) {
      this.logger.error('Reservation failed', error);
      throw error;
    }
  }

  async confirmReservation(
    dto: ConfirmReservationDto,
    userId: number
  ): Promise<ConfirmReservationResponseDto> {
    return this.reservationService.confirm(dto, userId);
  }

  async cancelReservation(token: string, userId: number): Promise<void> {
    return this.reservationService.cancel(token, userId);
  }

  async previewNumber(ctx: GenerateNumberContext): Promise<{
    previewNumber: string;
    nextSequence: number;
    isDefault: boolean;
  }> {
    const currentYear = new Date().getFullYear();
    const resetScope = `YEAR_${currentYear}`;

    const key: CounterKeyDto = {
      projectId: ctx.projectId,
      originatorOrganizationId: ctx.originatorOrganizationId,
      recipientOrganizationId: ctx.recipientOrganizationId || 0,
      correspondenceTypeId: ctx.typeId,
      subTypeId: ctx.subTypeId || 0,
      rfaTypeId: ctx.rfaTypeId || 0,
      disciplineId: ctx.disciplineId || 0,
      resetScope: resetScope,
    };

    const currentSeq = await this.counterService.getCurrentCounter(key);
    const nextSequence = currentSeq + 1;

    const { previewNumber, isDefault } = await this.formatService.format({
      projectId: ctx.projectId,
      correspondenceTypeId: ctx.typeId,
      subTypeId: ctx.subTypeId,
      rfaTypeId: ctx.rfaTypeId,
      disciplineId: ctx.disciplineId,
      sequence: nextSequence,
      resetScope: resetScope,
      year: currentYear,
      originatorOrganizationId: ctx.originatorOrganizationId,
      recipientOrganizationId: ctx.recipientOrganizationId,
    });

    return { previewNumber, nextSequence, isDefault };
  }

  /**
   * Generates a new number for a draft when its context changes.
   */
  async updateNumberForDraft(
    currentNumber: string,
    oldCtx: GenerateNumberContext,
    newCtx: GenerateNumberContext
  ): Promise<string> {
    const result = await this.generateNextNumber(newCtx);
    return result.number;
  }

  // --- Admin / Legacy ---

  async getTemplates(): Promise<DocumentNumberFormat[]> {
    return this.formatRepo.find({
      relations: ['project', 'correspondenceType'],
    });
  }

  async getTemplatesByProject(
    projectId: number | string
  ): Promise<DocumentNumberFormat[]> {
    const internalId = await this.uuidResolver.resolveProjectId(projectId);
    return this.formatRepo.find({
      where: { projectId: internalId },
      relations: ['project', 'correspondenceType'],
    });
  }

  async saveTemplate(
    dto: Partial<DocumentNumberFormat> & { projectId?: number | string }
  ): Promise<DocumentNumberFormat> {
    try {
      this.logger.log(`Saving numbering template: ${JSON.stringify(dto)}`);

      // Resolve project ID if it's a UUID/String
      if (dto.projectId && typeof dto.projectId === 'string') {
        dto.projectId = await this.uuidResolver.resolveProjectId(dto.projectId);
      }

      // Upsert logic: If no ID provided, check for existing template with same business key
      if (!dto.id) {
        const existing = await this.formatRepo.findOne({
          where: {
            projectId: Number(dto.projectId),
            correspondenceTypeId: dto.correspondenceTypeId
              ? Equal(dto.correspondenceTypeId)
              : IsNull(),
            disciplineId: dto.disciplineId || 0,
          },
        });
        if (existing) {
          this.logger.log(
            `Found existing template ID: ${existing.id} for business key, updating instead of creating.`
          );
          dto.id = existing.id;
        }
      }

      const result = await this.formatRepo.save(dto);
      this.logger.log(`Successfully saved template ID: ${result.id}`);
      return result;
    } catch (e: unknown) {
      this.logger.error(
        `Failed to save numbering template: ${e instanceof Error ? e.message : String(e)}`,
        e instanceof Error ? e.stack : undefined
      );
      throw e;
    }
  }

  async deleteTemplate(id: number) {
    return this.formatRepo.delete(id);
  }

  async getAuditLogs(limit: number) {
    return this.auditRepo.find({ take: limit, order: { createdAt: 'DESC' } });
  }

  async getErrorLogs(limit: number) {
    return this.errorRepo.find({ take: limit, order: { createdAt: 'DESC' } });
  }

  async getSequences(projectId?: number) {
    await Promise.resolve(projectId); // satisfy unused
    return [];
  }

  async setCounterValue(id: number, sequence: number) {
    await Promise.resolve(id); // satisfy unused
    await Promise.resolve(sequence);
    throw new BusinessException(
      'COUNTER_UPDATE_NOT_SUPPORTED',
      'Updating counter by single ID is not supported with composite keys',
      'ไม่รองรับการอัปเดต Counter แบบ Single ID กรุณาใช้ manualOverride',
      ['ใช้ manualOverride แทน']
    );
  }

  async manualOverride(dto: ManualOverrideDto, userId: number) {
    return this.manualOverrideService.applyOverride(dto, userId);
  }
  async voidAndReplace(dto: {
    documentNumber: string;
    reason: string;
    replace: boolean;
  }) {
    // 1. Find the audit log for this number to get context
    const lastAudit = await this.auditRepo.findOne({
      where: { documentNumber: dto.documentNumber },
      order: { createdAt: 'DESC' },
    });

    if (!lastAudit) {
      // If not found in audit, we can't easily regenerate with same context unless passed in dto.
      // For now, log a warning and return error or just log the void decision.
      this.logger.warn(
        `Void request for unknown number: ${dto.documentNumber}`
      );
      // Create a void audit anyway if possible?
      await this.logAudit({
        documentNumber: dto.documentNumber,
        counterKey: {},
        templateUsed: 'VOID_UNKNOWN',
        context: { userId: 0, ipAddress: '0.0.0.0' }, // System
        isSuccess: true,
        operation: 'VOID',
        status: 'VOID',
        newValue: 'VOIDED',
        metadata: { reason: dto.reason },
      });
      return { status: 'VOIDED_UNKNOWN_CONTEXT' };
    }

    // 2. Log VOID
    await this.logAudit({
      documentNumber: dto.documentNumber,
      counterKey: lastAudit.counterKey,
      templateUsed: lastAudit.templateUsed,
      context: { userId: 0, ipAddress: '0.0.0.0' }, // TODO: Pass userId from controller
      isSuccess: true,
      operation: 'VOID',
      status: 'VOID',
      oldValue: dto.documentNumber,
      newValue: 'VOIDED',
      metadata: { reason: dto.reason, replace: dto.replace },
    });

    if (dto.replace) {
      // 3. Generate Replacement
      // Parse context from lastAudit.counterKey?
      // GenerateNumberContext needs more than counterKey.
      // But we can reconstruct it.
      let context: GenerateNumberContext;
      try {
        const rawKey = lastAudit.counterKey;
        const key =
          typeof rawKey === 'string'
            ? (JSON.parse(rawKey) as Record<string, unknown>)
            : rawKey;

        context = {
          projectId: Number(key.projectId),
          typeId: Number(key.correspondenceTypeId),
          subTypeId: Number(key.subTypeId),
          rfaTypeId: Number(key.rfaTypeId),
          disciplineId: Number(key.disciplineId),
          originatorOrganizationId: Number(key.originatorOrganizationId) || 0,
          recipientOrganizationId: Number(key.recipientOrganizationId) || 0,
          userId: 0, // System replacement
        };

        const next = await this.generateNextNumber(context);
        return {
          status: 'REPLACED',
          oldNumber: dto.documentNumber,
          newNumber: next.number,
        };
      } catch (e) {
        this.logger.error(`Failed to replace number ${dto.documentNumber}`, e);
        return {
          status: 'VOIDED_REPLACE_FAILED',
          error: e instanceof Error ? e.message : String(e),
        };
      }
    }

    return { status: 'VOIDED' };
  }

  async cancelNumber(dto: {
    documentNumber: string;
    reason: string;
    projectId?: number;
  }) {
    // Similar to VOID but status CANCELLED
    const lastAudit = await this.auditRepo.findOne({
      where: { documentNumber: dto.documentNumber },
      order: { createdAt: 'DESC' },
    });

    const contextKey = lastAudit?.counterKey;

    await this.logAudit({
      documentNumber: dto.documentNumber,
      counterKey: contextKey || {},
      templateUsed: lastAudit?.templateUsed || 'CANCEL',
      context: {
        userId: 0,
        ipAddress: '0.0.0.0',
        projectId: dto.projectId || 0,
      },
      isSuccess: true,
      operation: 'CANCEL',
      status: 'CANCELLED',
      metadata: { reason: dto.reason },
    });

    return { status: 'CANCELLED' };
  }

  async bulkImport(items: ManualOverrideDto[]) {
    const results = { success: 0, failed: 0, errors: [] as string[] };

    // items expected to be ManualOverrideDto[] or similar
    // Actually bulk import usually means "Here is a list of EXISTING numbers used in legacy system"
    // So we should parse them and update counters if they are higher.

    // Implementation: For each item, likely delegate to ManualOverrideService if it fits schema.
    // Or if items is just a number of CSV rows?
    // Assuming items is parsed CSV rows.

    for (const item of items) {
      try {
        // Adapt item to ManualOverrideDto
        /*
          CSV columns: ProjectID, TypeID, OriginatorID, RecipientID, LastNumber
        */
        if (item.newLastNumber && item.correspondenceTypeId) {
          await this.manualOverrideService.applyOverride(item, 0); // 0 = System
          results.success++;
        }
      } catch (e) {
        results.failed++;
        results.errors.push(
          `Failed item ${JSON.stringify(item)}: ${e instanceof Error ? e.message : String(e)}`
        );
      }
    }
    return results;
  }

  private async logAudit(data: {
    documentNumber: string;
    counterKey: unknown;
    templateUsed: string;
    context: { projectId?: number; userId?: number; ipAddress?: string };
    isSuccess: boolean;
    operation: string;
    status?: string;
    oldValue?: string;
    newValue?: string;
    metadata?: Record<string, unknown>;
  }): Promise<DocumentNumberAudit> {
    const audit = this.auditRepo.create({
      documentNumber: data.documentNumber,
      counterKey: data.counterKey as Record<string, unknown>,
      templateUsed: data.templateUsed,
      isSuccess: data.isSuccess,
      operation: data.operation,
      status: data.status,
      oldValue: data.oldValue,
      newValue: data.newValue,
      metadata: data.metadata,
      userId: data.context.userId,
      ipAddress: data.context.ipAddress,
    });
    return this.auditRepo.save(audit);
  }

  private mapErrorType(error: Error): string {
    const name = error.name || '';
    const msg = error.message || '';
    if (name === 'ConflictException' || msg.includes('version'))
      return 'VERSION_CONFLICT';
    if (msg.includes('lock') || msg.includes('timeout')) return 'LOCK_TIMEOUT';
    if (msg.includes('Redis') || msg.includes('redis')) return 'REDIS_ERROR';
    if (msg.includes('duplicate') || msg.includes('Duplicate'))
      return 'DUPLICATE_NUMBER';
    if (msg.includes('validation') || msg.includes('Validation'))
      return 'VALIDATION_ERROR';
    if (msg.includes('exhausted') || msg.includes('maximum'))
      return 'SEQUENCE_EXHAUSTED';
    if (msg.includes('expired') || msg.includes('reservation'))
      return 'RESERVATION_EXPIRED';
    if (msg.includes('database') || msg.includes('query')) return 'DB_ERROR';
    return 'GENERATE_ERROR';
  }

  private async logError(error: unknown, ctx: unknown, operation: string) {
    try {
      const err = error instanceof Error ? error : new Error(String(error));
      const errEntity = this.errorRepo.create({
        errorMessage: err.message || 'Unknown Error',
        errorType: this.mapErrorType(err),
        contextData: {
          ...(typeof ctx === 'object' && ctx !== null
            ? (ctx as Record<string, unknown>)
            : {}),
          operation,
        },
      });
      await this.errorRepo.save(errEntity);
    } catch (e) {
      this.logger.error('Failed to log error to DB', e);
    }
  }
}
