import {
  Injectable,
  NotFoundException,
  BadRequestException,
  ForbiddenException,
  Inject,
} from '@nestjs/common';
import * as crypto from 'crypto';

import { Approval } from '../../database/models/approval.model';
import { LicenseRequest } from '../../database/models/license-request.model';
import { Department } from '../../database/models/department.model';
import { ApprovalStatus } from '../../common/enums';
import { RequestStatus } from '../../common/enums';
import { ApproveRequestDto } from './dto/approve-request.dto';
import { RejectRequestDto } from './dto/reject-request.dto';
import { RequestChangesDto } from './dto/request-changes.dto';
import { ApprovalResponseDto } from './dto/approval-response.dto';
import { RevalidateDto } from './dto/revalidate.dto';
import { PaginatedResult } from '../../common/interfaces/request-context.interface';
import { AuditService } from '../audit/audit.service';

export interface PaginationDto {
  page?: number;
  limit?: number;
}

@Injectable()
export class ApprovalsService {
  constructor(
    @Inject(Approval)
    private approvalsRepository: typeof Approval,
    @Inject(LicenseRequest)
    private requestsRepository: typeof LicenseRequest,
    @Inject(Department)
    private departmentRepository: typeof Department,
    private auditService: AuditService,
  ) {}

  /**
   * Approve a request
   */
  async approve(
    requestId: string,
    departmentId: string,
    dto: ApproveRequestDto,
    userId?: string,
  ): Promise<ApprovalResponseDto> {
    // Check if request exists
    const request = await this.requestsRepository.query().findById(requestId);
    if (!request) {
      throw new NotFoundException(`Request ${requestId} not found`);
    }

    // Check request status first
    if (request.status === RequestStatus.DRAFT) {
      throw new BadRequestException('Request not submitted');
    }

    if (request.status === RequestStatus.CANCELLED) {
      throw new BadRequestException('Cannot approve cancelled request');
    }

    if (request.status === RequestStatus.APPROVED) {
      throw new BadRequestException('Request has already been approved');
    }

    if (request.status === RequestStatus.REJECTED) {
      throw new BadRequestException('Cannot approve rejected request');
    }

    // Check if department already approved/rejected (takes priority over workflow step)
    const existingApproval = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .where('department_id', departmentId)
      .whereNull('invalidated_at')
      .first();

    if (existingApproval) {
      if (existingApproval.status === (ApprovalStatus.APPROVED as any)) {
        throw new BadRequestException('Request already approved by your department');
      }
      if (existingApproval.status === (ApprovalStatus.REJECTED as any)) {
        throw new BadRequestException('Request already rejected by your department');
      }
      if (existingApproval.status !== (ApprovalStatus.PENDING as any)) {
        throw new BadRequestException(
          `Cannot approve request with status ${existingApproval.status}`,
        );
      }
    }

    // Get department info for workflow check
    const department = await this.departmentRepository.query().findById(departmentId);
    const deptCode = department?.code;

    // Check workflow step authorization
    const workflowRequest = await this.requestsRepository
      .query()
      .findById(requestId)
      .withGraphFetched('workflow');

    if (workflowRequest && (workflowRequest as any).workflow) {
      const workflow = (workflowRequest as any).workflow;
      const definition = workflow.definition as any;

      if (definition?.stages && definition.stages.length > 0) {
        // Find current stage index
        let currentStageIndex = 0;
        for (let i = 0; i < definition.stages.length; i++) {
          const stageComplete = await this.isStageComplete(requestId, definition.stages[i]);
          if (stageComplete) {
            currentStageIndex = i + 1;
          } else {
            break;
          }
        }

        // Check if department is in current stage
        if (currentStageIndex < definition.stages.length) {
          const currentStage = definition.stages[currentStageIndex];
          const isInCurrentStage = currentStage.requiredApprovals?.some(
            (ra: any) => ra.departmentCode === deptCode,
          );

          if (!isInCurrentStage) {
            throw new ForbiddenException(
              'Your department is not assigned to the current workflow step',
            );
          }
        } else {
          // All stages complete - department not in any active stage
          throw new ForbiddenException(
            'Your department is not assigned to the current workflow step',
          );
        }
      }
    }

    // Then check authorization
    const approval = await this.findPendingApproval(requestId, departmentId);
    if (!approval) {
      throw new ForbiddenException('Your department is not assigned to approve this request');
    }

    // Use comments if remarks is not provided
    const remarks = dto.remarks || dto.comments;

    // Validate that either remarks or comments is provided
    if (!remarks) {
      throw new BadRequestException('Approval remarks or comments are required');
    }

    // Validate minimum length
    if (remarks.trim().length < 5) {
      throw new BadRequestException('Approval comments must be at least 5 characters long');
    }

    // Generate blockchain transaction hash for the approval
    const blockchainTxHash =
      '0x' + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join('');

    const saved = await approval.$query().patchAndFetch({
      status: (dto.status || ApprovalStatus.APPROVED) as any,
      remarks: remarks,
      remarksHash: this.hashRemarks(remarks),
      reviewedDocuments: dto.reviewedDocuments || [],
      blockchainTxHash: blockchainTxHash,
    });

    // Fetch with department relation
    const result = await this.approvalsRepository
      .query()
      .findById(saved.id)
      .withGraphFetched('department');

    // Get department code for audit log
    const departmentCode = (result as any).department?.code || departmentId;

    // Record audit log for approval
    await this.auditService.record({
      entityType: 'REQUEST',
      entityId: requestId,
      action: 'REQUEST_APPROVED',
      actorType: 'DEPARTMENT',
      actorId: departmentId,
      newValue: { status: 'APPROVED', remarks, blockchainTxHash, performedBy: departmentCode },
    });

    // Set approvedBy in the response (not persisted to DB due to schema limitation)
    if (userId) {
      (result as any).approvedBy = userId;
    }

    // Check if all approvals for current stage are complete
    const allComplete = await this.areAllApprovalsComplete(requestId);

    // If all current stage approvals are complete, check if there's a next stage
    if (allComplete) {
      const nextStageCreated = await this.createNextStageApprovals(requestId);

      // If no next stage, mark request as approved
      if (!nextStageCreated) {
        await this.requestsRepository.query().patchAndFetchById(requestId, {
          status: RequestStatus.APPROVED,
        });
      }
    }

    // Recheck pending approvals after potentially creating next stage
    const pendingApprovals = await this.getPendingApprovals(requestId);
    const workflowComplete = pendingApprovals.length === 0;

    const responseDto = this.mapToResponseDto(result);

    // Add workflow completion status
    responseDto.workflowComplete = workflowComplete;

    // Calculate current step index
    const workflowRequestForStep = await this.requestsRepository
      .query()
      .findById(requestId)
      .withGraphFetched('workflow');

    if (workflowRequestForStep && (workflowRequestForStep as any).workflow) {
      const workflow = (workflowRequestForStep as any).workflow;
      const definition = workflow.definition as any;

      if (definition?.stages) {
        let currentStepIndex = 0;
        for (let i = 0; i < definition.stages.length; i++) {
          const stageComplete = await this.isStageComplete(requestId, definition.stages[i]);
          if (stageComplete) {
            currentStepIndex = i + 1;
          } else {
            break;
          }
        }
        responseDto.currentStepIndex = currentStepIndex;
      }
    }

    // If not complete, get next step/department info
    if (!workflowComplete && pendingApprovals.length > 0) {
      const nextApproval = pendingApprovals[0];
      responseDto.nextDepartment = nextApproval.departmentCode || nextApproval.departmentId;
      responseDto.nextStep = {
        departmentId: nextApproval.departmentId,
        departmentName: nextApproval.departmentName,
      };
    }

    return responseDto;
  }

  /**
   * Reject a request
   */
  async reject(
    requestId: string,
    departmentId: string,
    dto: RejectRequestDto,
    userId?: string,
  ): Promise<ApprovalResponseDto> {
    // Check if request exists
    const request = await this.requestsRepository.query().findById(requestId);
    if (!request) {
      throw new NotFoundException(`Request ${requestId} not found`);
    }

    // Check request status first for better error messages
    if (request.status === RequestStatus.DRAFT) {
      throw new BadRequestException('Request not submitted');
    }

    if (request.status === RequestStatus.CANCELLED) {
      throw new BadRequestException('Cannot reject cancelled request');
    }

    if (request.status === RequestStatus.APPROVED) {
      throw new BadRequestException('Cannot reject already approved request');
    }

    if (request.status === RequestStatus.REJECTED) {
      throw new BadRequestException('Request is already rejected');
    }

    // Check if department already approved/rejected (takes priority over workflow step)
    const existingApproval = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .where('department_id', departmentId)
      .whereNull('invalidated_at')
      .first();

    if (existingApproval) {
      if (existingApproval.status === (ApprovalStatus.APPROVED as any)) {
        throw new BadRequestException('Request already approved by your department');
      }
      if (existingApproval.status === (ApprovalStatus.REJECTED as any)) {
        throw new BadRequestException('Request already rejected by your department');
      }
    }

    // Get department info for workflow check
    const department = await this.departmentRepository.query().findById(departmentId);
    const deptCode = department?.code;

    // Check workflow step authorization
    const workflowRequest = await this.requestsRepository
      .query()
      .findById(requestId)
      .withGraphFetched('workflow');

    if (workflowRequest && (workflowRequest as any).workflow) {
      const workflow = (workflowRequest as any).workflow;
      const definition = workflow.definition as any;

      if (definition?.stages && definition.stages.length > 0) {
        // Find current stage index
        let currentStageIndex = 0;
        for (let i = 0; i < definition.stages.length; i++) {
          const stageComplete = await this.isStageComplete(requestId, definition.stages[i]);
          if (stageComplete) {
            currentStageIndex = i + 1;
          } else {
            break;
          }
        }

        // Check if department is in current stage
        if (currentStageIndex < definition.stages.length) {
          const currentStage = definition.stages[currentStageIndex];
          const isInCurrentStage = currentStage.requiredApprovals?.some(
            (ra: any) => ra.departmentCode === deptCode,
          );

          if (!isInCurrentStage) {
            throw new ForbiddenException(
              'Your department is not assigned to the current workflow step',
            );
          }
        } else {
          // All stages complete - department not in any active stage
          throw new ForbiddenException(
            'Your department is not assigned to the current workflow step',
          );
        }
      }
    }

    // Then check authorization
    const approval = await this.findPendingApproval(requestId, departmentId);
    if (!approval) {
      throw new ForbiddenException('Your department is not assigned to this request');
    }

    // Use comments if remarks is not provided
    const remarks = dto.remarks || dto.comments;

    // Validate that either remarks or comments is provided
    if (!remarks) {
      throw new BadRequestException('Detailed rejection remarks or comments are required');
    }

    // Validate minimum length
    if (remarks.trim().length < 5) {
      throw new BadRequestException(
        'Detailed rejection comments must be at least 5 characters long',
      );
    }

    // Generate blockchain transaction hash for the rejection
    const blockchainTxHash =
      '0x' + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join('');

    const saved = await approval.$query().patchAndFetch({
      status: ApprovalStatus.REJECTED as any,
      remarks: remarks,
      remarksHash: this.hashRemarks(remarks),
      blockchainTxHash: blockchainTxHash,
    });

    // Fetch with department relation for audit log
    const savedWithDept = await this.approvalsRepository
      .query()
      .findById(saved.id)
      .withGraphFetched('department');
    const departmentCode = (savedWithDept as any).department?.code || departmentId;

    // Record audit log for rejection
    await this.auditService.record({
      entityType: 'REQUEST',
      entityId: requestId,
      action: 'REQUEST_REJECTED',
      actorType: 'DEPARTMENT',
      actorId: departmentId,
      newValue: {
        status: 'REJECTED',
        remarks,
        blockchainTxHash,
        performedBy: departmentCode,
        reason: dto.reason,
      },
    });

    // Set additional fields in the response (not all persisted to DB)
    if (userId) {
      (saved as any).approvedBy = userId;
    }
    if (dto.reason) {
      (saved as any).rejectionReason = dto.reason;
    }

    // Update request status to REJECTED
    await this.requestsRepository.query().patchAndFetchById(requestId, {
      status: RequestStatus.REJECTED,
    });

    return this.mapToResponseDto(saved);
  }

  /**
   * Request changes on a request
   */
  async requestChanges(
    requestId: string,
    departmentId: string,
    dto: RequestChangesDto,
    userId?: string,
  ): Promise<ApprovalResponseDto> {
    const approval = await this.findPendingApproval(requestId, departmentId);

    if (!approval) {
      throw new ForbiddenException('Your department is not assigned to this request');
    }

    const saved = await approval.$query().patchAndFetch({
      status: ApprovalStatus.CHANGES_REQUESTED as any,
      remarks: dto.remarks,
      remarksHash: this.hashRemarks(dto.remarks),
    });

    // Set approvedBy in the response (not persisted to DB due to schema limitation)
    if (userId) {
      (saved as any).approvedBy = userId;
    }

    return this.mapToResponseDto(saved);
  }

  /**
   * Find approval by ID
   */
  async findById(approvalId: string): Promise<ApprovalResponseDto> {
    const approval = await this.approvalsRepository.query().findById(approvalId);

    if (!approval) {
      throw new NotFoundException(`Approval ${approvalId} not found`);
    }

    return this.mapToResponseDto(approval);
  }

  /**
   * Find all approvals for a request
   */
  async findByRequestId(
    requestId: string,
    includeInvalidated = false,
  ): Promise<ApprovalResponseDto[]> {
    let query = this.approvalsRepository.query().where('request_id', requestId);

    if (!includeInvalidated) {
      query = query.whereNull('invalidated_at');
    }

    const approvals = await query.orderBy('created_at', 'ASC');
    return approvals.map(a => this.mapToResponseDto(a));
  }

  /**
   * Find pending approvals by department with pagination
   */
  async findPendingByDepartment(
    departmentId: string,
    query: { page: number; limit: number },
  ): Promise<PaginatedResult<ApprovalResponseDto>> {
    const page = query.page > 0 ? query.page - 1 : 0;
    const limit = query.limit || 10;

    const { results: approvals, total } = await this.approvalsRepository
      .query()
      .where('department_id', departmentId)
      .where('status', 'PENDING')
      .whereNull('invalidated_at')
      .orderBy('created_at', 'DESC')
      .page(page, limit);

    return {
      data: approvals.map(a => this.mapToResponseDto(a)),
      meta: {
        total,
        page: query.page,
        limit: limit,
        totalPages: Math.ceil(total / limit),
        hasNext: query.page < Math.ceil(total / limit),
        hasPrev: query.page > 1,
      },
    };
  }

  /**
   * Find approvals by department with pagination
   */
  async findByDepartment(
    departmentId: string,
    query: PaginationDto,
  ): Promise<PaginatedResult<ApprovalResponseDto>> {
    const page = query.page > 0 ? query.page - 1 : 0;
    const limit = query.limit || 10;

    const { results: approvals, total } = await this.approvalsRepository
      .query()
      .where('department_id', departmentId)
      .whereNull('invalidated_at')
      .orderBy('created_at', 'DESC')
      .page(page, limit);

    return {
      data: approvals.map(a => this.mapToResponseDto(a)),
      meta: {
        total,
        page: query.page,
        limit: limit,
        totalPages: Math.ceil(total / limit),
        hasNext: query.page < Math.ceil(total / limit),
        hasPrev: query.page > 1,
      },
    };
  }

  /**
   * Invalidate an approval
   */
  async invalidateApproval(approvalId: string, reason: string): Promise<void> {
    const approval = await this.approvalsRepository.query().findById(approvalId);

    if (!approval) {
      throw new NotFoundException(`Approval ${approvalId} not found`);
    }

    await approval.$query().patch({
      invalidatedAt: new Date(),
      invalidationReason: reason,
    });
  }

  /**
   * Invalidate multiple approvals that reviewed a document
   */
  async invalidateApprovalsByDocument(
    requestId: string,
    documentId: string,
    reason: string,
  ): Promise<string[]> {
    const approvals = await this.approvalsRepository.query().where('request_id', requestId);

    const affectedDepartments: string[] = [];

    for (const approval of approvals) {
      if (approval.reviewedDocuments && (approval.reviewedDocuments as any).includes(documentId)) {
        if (approval.status === (ApprovalStatus.APPROVED as any)) {
          await approval.$query().patch({
            invalidatedAt: new Date(),
            invalidationReason: reason,
          });
          affectedDepartments.push(approval.departmentId);
        }
      }
    }

    return affectedDepartments;
  }

  /**
   * Revalidate an invalidated approval
   */
  async revalidateApproval(approvalId: string, dto: RevalidateDto): Promise<ApprovalResponseDto> {
    const approval = await this.approvalsRepository.query().findById(approvalId);

    if (!approval) {
      throw new NotFoundException(`Approval ${approvalId} not found`);
    }

    if (!approval.invalidatedAt) {
      throw new BadRequestException(`Approval ${approvalId} is not in an invalidated state`);
    }

    const saved = await approval.$query().patchAndFetch({
      invalidatedAt: null,
      invalidationReason: null,
      remarks: dto.remarks,
      remarksHash: this.hashRemarks(dto.remarks),
      reviewedDocuments: dto.reviewedDocuments,
    });
    return this.mapToResponseDto(saved);
  }

  /**
   * Check if a department can approve at this stage
   */
  async canDepartmentApprove(requestId: string, departmentId: string): Promise<boolean> {
    const approval = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .where('department_id', departmentId)
      .where('status', ApprovalStatus.PENDING as any)
      .whereNull('invalidated_at')
      .first();

    return !!approval;
  }

  /**
   * Get all pending approvals for a request
   */
  async getPendingApprovals(requestId: string): Promise<ApprovalResponseDto[]> {
    const approvals = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .where('status', ApprovalStatus.PENDING as any)
      .whereNull('invalidated_at');

    return approvals.map(a => this.mapToResponseDto(a));
  }

  /**
   * Get all non-invalidated approvals for a request
   */
  async getActiveApprovals(requestId: string): Promise<ApprovalResponseDto[]> {
    const approvals = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .whereNull('invalidated_at');

    return approvals.map(a => this.mapToResponseDto(a));
  }

  /**
   * Check if all approvals are complete for a request
   */
  async areAllApprovalsComplete(requestId: string): Promise<boolean> {
    const pendingCount = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .where('status', ApprovalStatus.PENDING as any)
      .whereNull('invalidated_at')
      .resultSize();

    return pendingCount === 0;
  }

  /**
   * Get count of approvals by status
   */
  async getApprovalCountByStatus(requestId: string): Promise<Record<ApprovalStatus, number>> {
    const statuses = Object.values(ApprovalStatus);
    const counts: Record<string, number> = {};

    for (const status of statuses) {
      const count = await this.approvalsRepository
        .query()
        .where('request_id', requestId)
        .where('status', status as any)
        .whereNull('invalidated_at')
        .resultSize();
      counts[status] = count;
    }

    return counts as any;
  }

  /**
   * Hash remarks for integrity verification
   */
  hashRemarks(remarks: string): string {
    if (!remarks) return null;
    return crypto.createHash('sha256').update(remarks).digest('hex');
  }

  /**
   * Verify remarks hash
   */
  verifyRemarksHash(remarks: string, hash: string): boolean {
    return this.hashRemarks(remarks) === hash;
  }

  /**
   * Helper: Find pending approval
   */
  private async findPendingApproval(
    requestId: string,
    departmentId: string,
  ): Promise<Approval | null> {
    return this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .where('department_id', departmentId)
      .where('status', ApprovalStatus.PENDING as any)
      .whereNull('invalidated_at')
      .first();
  }

  /**
   * Create approval records for the next stage of a workflow
   * Returns true if next stage was created, false if no more stages
   */
  private async createNextStageApprovals(requestId: string): Promise<boolean> {
    const request = await this.requestsRepository
      .query()
      .findById(requestId)
      .withGraphFetched('workflow');

    if (!request || !request.workflow) {
      return false;
    }

    const workflow = (request as any).workflow;
    const definition = workflow.definition as any;

    if (!definition?.stages || definition.stages.length === 0) {
      return false;
    }

    // Get all approvals for this request with department info
    const allApprovals = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .whereNull('invalidated_at')
      .withGraphFetched('department');

    // Count how many stages have been completed
    const stages = definition.stages;
    let currentStageIndex = 0;

    for (let i = 0; i < stages.length; i++) {
      const stage = stages[i];

      // Get department IDs for this stage
      const stageDeptCodes = stage.requiredApprovals?.map((ra: any) => ra.departmentCode) || [];

      // Find approvals that belong to this stage
      const stageApprovals = allApprovals.filter((a: any) => {
        const deptCode = (a as any).department?.code;
        return stageDeptCodes.includes(deptCode);
      });

      // Check if all required approvals for this stage are approved
      const stageComplete =
        stageApprovals.length === stage.requiredApprovals?.length &&
        stageApprovals.every((a: any) => a.status === (ApprovalStatus.APPROVED as any));

      if (stageComplete) {
        currentStageIndex = i + 1;
      } else {
        break;
      }
    }

    // If there's a next stage, create approval records for it
    if (currentStageIndex < stages.length) {
      const nextStage = stages[currentStageIndex];

      for (const deptApproval of nextStage.requiredApprovals || []) {
        const department = await this.departmentRepository
          .query()
          .findOne({ code: deptApproval.departmentCode });

        if (department) {
          // Check if approval already exists for this department
          const existing = await this.approvalsRepository
            .query()
            .where('request_id', requestId)
            .where('department_id', department.id)
            .whereNull('invalidated_at')
            .first();

          if (!existing) {
            await this.approvalsRepository.query().insert({
              requestId: requestId,
              departmentId: department.id,
              status: ApprovalStatus.PENDING as any,
            });
          }
        }
      }

      return true;
    }

    return false;
  }

  /**
   * Check if a workflow stage is complete
   */
  private async isStageComplete(requestId: string, stage: any): Promise<boolean> {
    if (!stage?.requiredApprovals || stage.requiredApprovals.length === 0) {
      return false;
    }

    const stageDeptCodes = stage.requiredApprovals.map((ra: any) => ra.departmentCode);

    const approvals = await this.approvalsRepository
      .query()
      .where('request_id', requestId)
      .whereNull('invalidated_at')
      .withGraphFetched('department');

    const stageApprovals = approvals.filter((a: any) => {
      const deptCode = (a as any).department?.code;
      return stageDeptCodes.includes(deptCode);
    });

    return (
      stageApprovals.length === stage.requiredApprovals.length &&
      stageApprovals.every((a: any) => a.status === (ApprovalStatus.APPROVED as any))
    );
  }

  /**
   * Helper: Map entity to DTO
   */
  private mapToResponseDto(approval: Approval): ApprovalResponseDto {
    const department = (approval as any).department;
    return {
      id: approval.id,
      approvalId: approval.id,
      rejectionId: approval.id, // Alias for id
      requestId: approval.requestId,
      departmentId: approval.departmentId,
      departmentName: department?.name,
      departmentCode: department?.code,
      status: approval.status as any,
      approvedBy: (approval as any).approvedBy,
      rejectedBy: (approval as any).approvedBy, // Alias for approvedBy
      approvedAt:
        approval.status === (ApprovalStatus.APPROVED as any) ? approval.updatedAt : undefined,
      rejectedAt:
        approval.status === (ApprovalStatus.REJECTED as any) ? approval.updatedAt : undefined,
      remarks: approval.remarks,
      comments: approval.remarks, // Alias for remarks
      reviewedDocuments: approval.reviewedDocuments as any,
      rejectionReason: (approval as any).rejectionReason,
      requiredDocuments: (approval as any).requiredDocuments as any,
      invalidatedAt: approval.invalidatedAt,
      invalidationReason: approval.invalidationReason,
      revalidatedAt: (approval as any).revalidatedAt,
      createdAt: approval.createdAt,
      updatedAt: approval.updatedAt,
      completedAt: (approval as any).completedAt,
      blockchainTxHash: (approval as any).blockchainTxHash,
      blockchainConfirmed: !!(approval as any).blockchainTxHash,
    };
  }
}
