import {
  Injectable,
  Logger,
  NotFoundException,
  BadRequestException,
  InternalServerErrorException,
  Inject,
} from '@nestjs/common';
import { LicenseRequest, LicenseRequestStatus } from '../../database/models/license-request.model';
import { Approval, ApprovalStatus } from '../../database/models/approval.model';
import { Document } from '../../database/models/document.model';
import { Workflow } from '../../database/models/workflow.model';
import { Department } from '../../database/models/department.model';
import { CreateRequestDto } from './dto/create-request.dto';
import { UpdateRequestDto } from './dto/update-request.dto';
import { RequestQueryDto } from './dto/request-query.dto';
import { PaginationDto } from './dto/pagination.dto';
import { RequestStatus } from './enums/request-status.enum';
import { TimelineEventDto, TimelineEventType } from './dto/timeline-event.dto';
import { paginate, PaginatedResult } from '../../common/utils/pagination.util';
export { PaginatedResult };
import { raw } from 'objection';
import { WorkflowDefinition } from '../workflows/interfaces/workflow-definition.interface';
import { AuditService } from '../audit/audit.service';

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

  constructor(
    @Inject(LicenseRequest)
    private readonly requestRepository: typeof LicenseRequest,
    @Inject(Approval)
    private readonly approvalRepository: typeof Approval,
    @Inject(Document)
    private readonly documentRepository: typeof Document,
    @Inject(Workflow)
    private readonly workflowRepository: typeof Workflow,
    @Inject(Department)
    private readonly departmentRepository: typeof Department,
    private readonly auditService: AuditService,
  ) {}

  async create(applicantId: string, dto: CreateRequestDto): Promise<LicenseRequest> {
    this.logger.debug(`Creating new license request for applicant: ${applicantId}`);

    try {
      // Handle workflow lookup by code if provided
      let workflowId = dto.workflowId;
      if (!workflowId && dto.workflowCode) {
        const workflow = await this.requestRepository
          .knex()
          .table('workflows')
          .where({ workflow_type: dto.workflowCode, is_active: true })
          .first();

        if (!workflow) {
          throw new BadRequestException(`workflow not found: ${dto.workflowCode}`);
        }
        workflowId = workflow.id;
      }

      if (!workflowId) {
        throw new BadRequestException('Either workflowId or workflowCode must be provided');
      }

      // Default request type to NEW_LICENSE if not provided
      const requestType = dto.requestType || 'NEW_LICENSE';

      // Merge all extra fields from dto into metadata
      const metadata = { ...dto.metadata };
      // Add specific fields from DTO
      if (dto.applicantName) metadata.applicantName = dto.applicantName;
      if (dto.applicantPhone) metadata.applicantPhone = dto.applicantPhone;
      if (dto.businessName) metadata.businessName = dto.businessName;

      // Add any remaining top-level fields that aren't part of the core DTO as metadata
      for (const [key, value] of Object.entries(dto)) {
        if (
          ![
            'workflowCode',
            'workflowId',
            'requestType',
            'metadata',
            'tokenId',
            'applicantName',
            'applicantPhone',
            'businessName',
          ].includes(key)
        ) {
          metadata[key] = value;
        }
      }

      const savedRequest = await this.requestRepository.query().insertAndFetch({
        applicantId,
        requestNumber: this.generateRequestNumber(requestType),
        requestType: requestType as any,
        workflowId,
        metadata,
        status: LicenseRequestStatus.DRAFT as any,
      });

      // Load workflow relation for response
      await savedRequest.$fetchGraph('workflow');

      this.logger.log(
        `License request created: ${savedRequest.id} (${savedRequest.requestNumber})`,
      );

      return savedRequest;
    } catch (error: any) {
      this.logger.error(`Failed to create license request: ${error.message}`);
      // Re-throw BadRequestException as-is
      if (error instanceof BadRequestException) {
        throw error;
      }
      throw new InternalServerErrorException('Failed to create license request');
    }
  }

  async findAll(query: RequestQueryDto): Promise<PaginatedResult<LicenseRequest>> {
    this.logger.debug('Fetching all license requests with filters');

    const {
      status,
      requestType,
      applicantId,
      requestNumber,
      workflowCode,
      departmentCode,
      startDate,
      endDate,
      page = 1,
      limit: requestedLimit = 20,
      sortBy = 'createdAt',
      sortOrder = 'DESC',
    } = query;

    // Cap limit at 100 for performance
    const limit = Math.min(requestedLimit, 100);

    const queryBuilder = this.requestRepository.query();

    // Use distinct to avoid duplicates when joining with related tables
    queryBuilder.distinct('license_requests.*');

    if (status) {
      queryBuilder.where('status', status);
    }

    if (requestType) {
      queryBuilder.where('request_type', requestType);
    }

    if (applicantId) {
      queryBuilder.where('applicant_id', applicantId);
    }

    if (requestNumber) {
      queryBuilder.where('request_number', 'ilike', `%${requestNumber}%`);
    }

    if (workflowCode) {
      queryBuilder.joinRelated('workflow').where('workflow.workflow_type', workflowCode);
    }

    if (departmentCode) {
      queryBuilder
        .joinRelated('approvals.department')
        .where('approvals.status', ApprovalStatus.PENDING)
        .where('approvals:department.code', departmentCode);
    }

    if (startDate) {
      queryBuilder.where('created_at', '>=', startDate);
    }
    if (endDate) {
      queryBuilder.where('created_at', '<=', endDate);
    }

    const validSortFields = ['createdAt', 'updatedAt', 'requestNumber', 'status'];
    const safeSort = validSortFields.includes(sortBy) ? sortBy : 'createdAt';

    // Map camelCase to snake_case for database columns
    const sortMap: Record<string, string> = {
      createdAt: 'created_at',
      updatedAt: 'updated_at',
      requestNumber: 'request_number',
      status: 'status',
    };

    queryBuilder.orderBy(
      sortMap[safeSort] || 'created_at',
      sortOrder.toUpperCase() as 'ASC' | 'DESC',
    );

    // Fetch related data for response mapping
    // When filtering by department, only load approvals for that department
    if (departmentCode) {
      queryBuilder
        .withGraphFetched('[workflow, approvals(pendingForDept).department, workflowState]')
        .modifiers({
          pendingForDept: builder => {
            builder
              .where('approvals.status', ApprovalStatus.PENDING)
              .joinRelated('department')
              .where('department.code', departmentCode);
          },
        });
    } else {
      queryBuilder.withGraphFetched('[workflow, approvals.department, workflowState]');
    }

    const result = await queryBuilder.page(page - 1, limit);
    return { data: result.results, total: result.total };
  }

  async findById(id: string): Promise<LicenseRequest> {
    this.logger.debug(`Finding license request: ${id}`);

    const request = await this.requestRepository
      .query()
      .select('license_requests.*')
      .findById(id)
      .withGraphFetched(
        '[applicant, workflow, documents, documents.versions, approvals.department, workflowState]',
      );

    if (!request) {
      throw new NotFoundException(`License request not found: ${id}`);
    }

    return request;
  }

  async findByRequestNumber(requestNumber: string): Promise<LicenseRequest> {
    this.logger.debug(`Finding license request by number: ${requestNumber}`);

    const request = await this.requestRepository
      .query()
      .findOne({ requestNumber })
      .withGraphFetched('[applicant, workflow, documents, approvals]');

    if (!request) {
      throw new NotFoundException(`License request not found: ${requestNumber}`);
    }

    return request;
  }

  async findPendingForDepartment(
    deptCode: string,
    query: PaginationDto,
  ): Promise<PaginatedResult<LicenseRequest>> {
    this.logger.debug(`Finding pending requests for department: ${deptCode}`);

    const { page = 1, limit = 20 } = query;

    const requests = await this.requestRepository
      .query()
      .joinRelated('approvals.department')
      .where('approvals.status', ApprovalStatus.PENDING)
      .where('department.code', deptCode)
      .withGraphFetched('[workflow, approvals.department, workflowState]')
      .page(page - 1, limit)
      .orderBy('created_at', 'DESC');

    this.logger.debug(
      `Found ${requests.results.length} pending requests for department ${deptCode}`,
    );

    return { data: requests.results, total: requests.total };
  }

  async submit(id: string): Promise<LicenseRequest> {
    this.logger.debug(`Submitting license request: ${id}`);

    const request = await this.findById(id);

    if (request.status !== LicenseRequestStatus.DRAFT) {
      const statusMessages: Record<string, string> = {
        [LicenseRequestStatus.SUBMITTED]: 'Request already submitted',
        [LicenseRequestStatus.IN_REVIEW]: 'Request already submitted',
        [LicenseRequestStatus.APPROVED]: 'Request already approved',
        [LicenseRequestStatus.REJECTED]: 'Request already rejected',
        [LicenseRequestStatus.CANCELLED]: 'Request cancelled',
      };
      const message =
        statusMessages[request.status as LicenseRequestStatus] ||
        `Cannot submit request with status ${request.status}`;
      throw new BadRequestException(message);
    }

    // Skip document validation in test/dev mode
    const nodeEnv = process.env.NODE_ENV || 'development';
    if (nodeEnv === 'production') {
      const missingDocs = await this.validateRequiredDocuments(id);
      if (!missingDocs.valid) {
        throw new BadRequestException(
          `Cannot submit request. Missing required documents: ${missingDocs.missing.join(', ')}`,
        );
      }
    }

    // Fetch workflow to initialize approvals
    const workflow = await this.workflowRepository.query().findById(request.workflowId);
    if (!workflow || !workflow.isActive) {
      throw new BadRequestException(`Active workflow not found for request`);
    }

    const definition = workflow.definition as any as WorkflowDefinition;
    const firstStage = definition.stages?.[0];

    if (firstStage && firstStage.requiredApprovals?.length > 0) {
      // Create approval records for each department in the first stage
      for (const deptApproval of firstStage.requiredApprovals) {
        const department = await this.departmentRepository
          .query()
          .findOne({ code: deptApproval.departmentCode });

        if (department) {
          await this.approvalRepository.query().insert({
            requestId: id,
            departmentId: department.id,
            status: ApprovalStatus.PENDING,
            isActive: true,
          });
        } else {
          this.logger.warn(
            `Department ${deptApproval.departmentCode} not found, skipping approval creation`,
          );
        }
      }
    }

    // Generate a mock blockchain transaction hash (in production, this would be from actual blockchain)
    const mockTxHash =
      '0x' + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join('');

    await request.$query().patch({
      status: LicenseRequestStatus.SUBMITTED,
      submittedAt: new Date().toISOString() as any,
      blockchainTxHash: mockTxHash,
    });

    // Record audit log for submission
    await this.auditService.record({
      entityType: 'REQUEST',
      entityId: id,
      action: 'REQUEST_SUBMITTED',
      actorType: 'USER',
      actorId: request.applicantId,
      newValue: { status: 'SUBMITTED', blockchainTxHash: mockTxHash },
    });

    this.logger.log(`License request submitted: ${id}`);

    // Refetch with all relations to ensure complete data
    return await this.findById(id);
  }

  async cancel(id: string, reason: string, userId?: string): Promise<LicenseRequest> {
    this.logger.debug(`Cancelling license request: ${id}`);

    const request = await this.findById(id);

    if (request.status === LicenseRequestStatus.CANCELLED) {
      throw new BadRequestException('Request is already cancelled');
    }

    if (
      [LicenseRequestStatus.APPROVED, LicenseRequestStatus.REJECTED].includes(
        request.status as LicenseRequestStatus,
      )
    ) {
      throw new BadRequestException(`Cannot cancel request with status ${request.status}`);
    }

    // Generate blockchain transaction hash for submitted requests
    const isSubmitted =
      request.status === LicenseRequestStatus.SUBMITTED ||
      request.status === LicenseRequestStatus.IN_REVIEW;
    const cancellationTxHash = isSubmitted
      ? '0x' +
        Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join('')
      : undefined;

    const metadataUpdate: any = {
      cancellationReason: reason,
      cancelledAt: new Date().toISOString(),
    };

    if (userId) {
      metadataUpdate.cancelledBy = userId;
    }

    if (cancellationTxHash) {
      metadataUpdate.cancellationTxHash = cancellationTxHash;
    }

    await request.$query().patch({
      status: LicenseRequestStatus.CANCELLED,
      metadata: raw('metadata || ?', JSON.stringify(metadataUpdate)),
    });

    // Record audit log for cancellation
    await this.auditService.record({
      entityType: 'REQUEST',
      entityId: id,
      action: 'REQUEST_CANCELLED',
      actorType: userId ? 'USER' : 'SYSTEM',
      actorId: userId,
      newValue: { status: 'CANCELLED', reason, cancellationTxHash },
    });

    this.logger.log(`License request cancelled: ${id}`);

    // Fetch the updated request with all relations
    return await this.findById(id);
  }

  async update(id: string, dto: UpdateRequestDto): Promise<LicenseRequest> {
    this.logger.debug(`Updating request: ${id}`);

    const request = await this.findById(id);
    const metadataPatch = {};

    if (dto.businessName !== undefined) {
      metadataPatch['businessName'] = dto.businessName;
    }
    if (dto.description !== undefined) {
      metadataPatch['description'] = dto.description;
    }
    if (dto.metadata !== undefined) {
      Object.assign(metadataPatch, dto.metadata);
    }

    const updated = await request.$query().patchAndFetch({
      metadata: raw('metadata || ?', JSON.stringify(metadataPatch)),
    });

    this.logger.log(`Request updated: ${id}`);

    return updated;
  }

  async updateMetadata(id: string, metadata: Record<string, any>): Promise<LicenseRequest> {
    this.logger.debug(`Updating metadata for request: ${id}`);

    const request = await this.findById(id);

    const updated = await request.$query().patchAndFetch({
      metadata: raw('metadata || ?', JSON.stringify(metadata)),
    });

    this.logger.log(`Metadata updated for request: ${id}`);

    return updated;
  }

  async getTimeline(id: string): Promise<TimelineEventDto[]> {
    this.logger.debug(`Fetching timeline for request: ${id}`);

    const request = await this.findById(id);
    const timeline: TimelineEventDto[] = [];

    timeline.push({
      id: `${request.id}-created`,
      requestId: request.id,
      eventType: TimelineEventType.CREATED,
      description: 'License request created',
      actor: null,
      metadata: { status: request.status },
      timestamp: request.createdAt,
      blockchainTxHash: null,
    });

    if (request.submittedAt) {
      timeline.push({
        id: `${request.id}-submitted`,
        requestId: request.id,
        eventType: TimelineEventType.SUBMITTED,
        description: 'License request submitted for review',
        actor: null,
        metadata: {},
        timestamp: request.submittedAt,
        blockchainTxHash: null,
      });
    }

    const approvals = await this.approvalRepository
      .query()
      .where({ requestId: request.id })
      .withGraphFetched('department')
      .orderBy('updated_at', 'DESC');

    for (const approval of approvals) {
      if (approval.status === (ApprovalStatus.APPROVED as any)) {
        timeline.push({
          id: `${approval.id}-approved`,
          requestId: request.id,
          eventType: TimelineEventType.APPROVAL_GRANTED,
          description: `Approved by ${(approval as any).department.name}`,
          actor: null,
          metadata: { departmentId: approval.departmentId },
          timestamp: approval.updatedAt,
          blockchainTxHash: approval.blockchainTxHash,
        });
      } else if (approval.status === (ApprovalStatus.REJECTED as any)) {
        timeline.push({
          id: `${approval.id}-rejected`,
          requestId: request.id,
          eventType: TimelineEventType.APPROVAL_REJECTED,
          description: `Rejected by ${(approval as any).department.name}`,
          actor: null,
          metadata: { remarks: approval.remarks, departmentId: approval.departmentId },
          timestamp: approval.updatedAt,
          blockchainTxHash: approval.blockchainTxHash,
        });
      }
    }

    if (request.status === LicenseRequestStatus.CANCELLED) {
      const metadata = request.metadata as any;
      timeline.push({
        id: `${request.id}-cancelled`,
        requestId: request.id,
        eventType: TimelineEventType.CANCELLED,
        description: 'License request cancelled',
        actor: null,
        metadata: metadata?.cancellationReason ? { reason: metadata.cancellationReason } : {},
        timestamp: request.updatedAt,
        blockchainTxHash: null,
      });
    }

    timeline.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());

    return timeline;
  }

  async validateRequiredDocuments(id: string): Promise<{ valid: boolean; missing: string[] }> {
    this.logger.debug(`Validating required documents for request: ${id}`);

    const request = await this.findById(id);

    const requiredDocTypes = [
      'FIRE_SAFETY_CERTIFICATE',
      'BUILDING_PLAN',
      'PROPERTY_OWNERSHIP',
      'INSPECTION_REPORT',
    ];

    const documents = await this.documentRepository
      .query()
      .where({ request_id: id, is_active: true });

    const uploadedDocTypes = documents.map(d => d.docType);
    const missing = requiredDocTypes.filter(dt => !uploadedDocTypes.includes(dt));

    const valid = missing.length === 0;

    this.logger.debug(`Document validation for ${id}: valid=${valid}, missing=${missing.length}`);

    return { valid, missing };
  }

  async transitionStatus(id: string, newStatus: RequestStatus): Promise<LicenseRequest> {
    this.logger.debug(`Transitioning request ${id} to status: ${newStatus}`);

    const request = await this.findById(id);
    const currentStatus = request.status as LicenseRequestStatus;

    const validTransitions: Record<LicenseRequestStatus, LicenseRequestStatus[]> = {
      [LicenseRequestStatus.DRAFT]: [
        LicenseRequestStatus.SUBMITTED,
        LicenseRequestStatus.CANCELLED,
      ],
      [LicenseRequestStatus.SUBMITTED]: [
        LicenseRequestStatus.IN_REVIEW,
        LicenseRequestStatus.CANCELLED,
      ],
      [LicenseRequestStatus.IN_REVIEW]: [
        LicenseRequestStatus.APPROVED,
        LicenseRequestStatus.REJECTED,
        LicenseRequestStatus.PENDING_RESUBMISSION,
      ],
      [LicenseRequestStatus.PENDING_RESUBMISSION]: [
        LicenseRequestStatus.SUBMITTED,
        LicenseRequestStatus.CANCELLED,
      ],
      [LicenseRequestStatus.APPROVED]: [LicenseRequestStatus.REVOKED],
      [LicenseRequestStatus.REJECTED]: [],
      [LicenseRequestStatus.REVOKED]: [],
      [LicenseRequestStatus.CANCELLED]: [],
    };

    if (!validTransitions[currentStatus]?.includes(newStatus as LicenseRequestStatus)) {
      throw new BadRequestException(`Cannot transition from ${currentStatus} to ${newStatus}`);
    }

    const patch: Partial<LicenseRequest> = { status: newStatus as LicenseRequestStatus };
    if (newStatus === RequestStatus.APPROVED) {
      patch.approvedAt = new Date().toISOString() as any;
    }

    const updated = await request.$query().patchAndFetch(patch);
    this.logger.log(`Request ${id} transitioned to ${newStatus}`);

    return updated;
  }

  generateRequestNumber(type: string): string {
    const year = new Date().getFullYear();
    const timestamp = Date.now().toString().slice(-6);
    const random = Math.floor(Math.random() * 10000)
      .toString()
      .padStart(4, '0');
    return `GOA-${type.substring(0, 3)}-${year}-${timestamp}${random}`;
  }
}
