import { ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import { AuditAction, AuditEntityType } from '@aechr/shared';
import { AssignmentStatus, Prisma, QuestionFieldType, SubmissionFinalStatus } from '@prisma/client';
import type { JwtAuthUser } from '../../common/interfaces/jwt-auth-user.interface';
import { buildPagination, buildSoftDeleteWhere } from '../../common/prisma/pagination';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import { SaveSubmissionDto } from './dto/save-submission.dto';
import { SubmissionQueryDto } from './dto/submission-query.dto';

function resolveReportMonthLabel(periodMonth?: number | null, periodYear?: number | null, startDate?: Date) {
  if (periodMonth && periodYear) {
    return new Date(Date.UTC(periodYear, periodMonth - 1, 1)).toLocaleString('en-US', {
      month: 'long',
      year: 'numeric',
      timeZone: 'UTC',
    });
  }

  if (startDate) {
    return startDate.toLocaleString('en-US', {
      month: 'long',
      year: 'numeric',
      timeZone: 'UTC',
    });
  }

  return '';
}

@Injectable()
export class SubmissionsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  async getAssignmentForm(assignmentId: string, user: JwtAuthUser) {
    const assignment = await this.loadAssignmentForUser(assignmentId, user);
    return {
      assignment: {
        id: assignment.id,
        status: assignment.status,
        isEnabled: assignment.isEnabled,
        reopenUntil: assignment.reopenUntil,
      },
      campaign: assignment.campaign,
      template: assignment.campaign.template,
      employeeContext: {
        designation: assignment.employee.designation,
        reportingManager: assignment.employee.manager?.name ?? '',
        reportMonth: resolveReportMonthLabel(
          assignment.campaign.periodMonth,
          assignment.campaign.periodYear,
          assignment.campaign.startDate,
        ),
      },
      questions: assignment.campaign.template.questions.map((mapping) => ({
        mappingId: mapping.id,
        sortOrder: mapping.sortOrder,
        sectionName: mapping.sectionName,
        isRequired: mapping.isRequiredOverride ?? mapping.question.isRequired,
        configJson: mapping.configJson,
        question: mapping.question,
      })),
      submission: assignment.submissions[0] ?? null,
    };
  }

  async saveDraft(assignmentId: string, dto: SaveSubmissionDto, user: JwtAuthUser) {
    const assignment = await this.loadAssignmentForUser(assignmentId, user);
    const submission = await this.persistSubmission(assignment, dto, user, false);
    return submission;
  }

  async submit(assignmentId: string, dto: SaveSubmissionDto, user: JwtAuthUser) {
    const assignment = await this.loadAssignmentForUser(assignmentId, user);
    this.validateRequiredAnswers(assignment, dto);
    const submission = await this.persistSubmission(assignment, dto, user, true);
    return submission;
  }

  async findOne(id: string) {
    const submission = await this.prisma.assessmentSubmission.findUnique({
      where: { id },
      include: {
        assignment: { include: { employee: true, campaign: true } },
        answers: { include: { question: true } },
      },
    });
    if (!submission) throw new NotFoundException('Submission not found');
    return submission;
  }

  async list(query: SubmissionQueryDto) {
    const { skip, take, meta } = buildPagination(query);
    const where: Prisma.AssessmentSubmissionWhereInput = {
      ...buildSoftDeleteWhere(query.includeDeleted),
      ...(query.assignmentId ? { assignmentId: query.assignmentId } : {}),
      ...(query.employeeId ? { assignment: { employeeId: query.employeeId } } : {}),
      ...(query.finalStatus ? { finalStatus: query.finalStatus } : {}),
    };
    const [items, total] = await this.prisma.$transaction([
      this.prisma.assessmentSubmission.findMany({
        where,
        skip,
        take,
        include: {
          assignment: { include: { employee: true, campaign: true } },
          answers: true,
        },
        orderBy: { createdAt: query.sortOrder },
      }),
      this.prisma.assessmentSubmission.count({ where }),
    ]);
    return { items, total, ...meta };
  }

  private async loadAssignmentForUser(assignmentId: string, user: JwtAuthUser) {
    const assignment = await this.prisma.assessmentAssignment.findUnique({
      where: { id: assignmentId },
      include: {
        campaign: {
          include: {
            template: {
              include: {
                questions: {
                  where: { deletedAt: null },
                  include: { question: true },
                  orderBy: { sortOrder: 'asc' },
                },
              },
            },
          },
        },
        employee: {
          include: {
            manager: {
              select: {
                id: true,
                name: true,
                email: true,
              },
            },
          },
        },
        submissions: {
          where: { deletedAt: null },
          include: {
            answers: {
              where: { deletedAt: null },
              orderBy: { updatedAt: 'asc' },
            },
          },
          orderBy: { updatedAt: 'desc' },
          take: 1,
        },
      },
    });
    if (!assignment || assignment.deletedAt) throw new NotFoundException('Assignment not found');
    if (assignment.employeeId !== user.sub) throw new ForbiddenException('Assignment does not belong to current user');
    if (!assignment.isEnabled) throw new ConflictException('Assignment is disabled');
    if (assignment.status === AssignmentStatus.LOCKED || assignment.status === AssignmentStatus.MISSED) {
      throw new ConflictException('Assignment is no longer open for submission');
    }
    return assignment;
  }

  private validateRequiredAnswers(assignment: Awaited<ReturnType<SubmissionsService['loadAssignmentForUser']>>, dto: SaveSubmissionDto) {
    const requiredMappings = assignment.campaign.template.questions.filter(
      (mapping) => (mapping.isRequiredOverride ?? mapping.question.isRequired) === true,
    );
    const answersByQuestionId = new Map(dto.answers.map((answer) => [answer.questionId, answer]));

    for (const mapping of requiredMappings) {
      const answer = answersByQuestionId.get(mapping.questionId);
      const hasText = !!answer?.answerText?.trim();
      const hasJson =
        answer?.answerJson !== undefined &&
        answer.answerJson !== null &&
        (!(Array.isArray(answer.answerJson)) || answer.answerJson.length > 0);

      if (!hasText && !hasJson) {
        throw new ConflictException(`Missing required answer for question ${mapping.question.label}`);
      }
    }

    for (const mapping of assignment.campaign.template.questions) {
      const answer = answersByQuestionId.get(mapping.questionId);
      if (!answer) continue;
      this.validateAnswerValue(mapping.question.fieldType, mapping.question.optionsJson, answer.answerJson);
    }
  }

  private validateAnswerValue(
    fieldType: QuestionFieldType,
    optionsJson: Prisma.JsonValue | null,
    answerJson: unknown,
  ) {
    const selectableTypes = new Set<QuestionFieldType>([
      QuestionFieldType.SELECT,
      QuestionFieldType.MULTI_SELECT,
      QuestionFieldType.RADIO,
      QuestionFieldType.CHECKBOX,
      QuestionFieldType.YES_NO,
    ]);

    if (!selectableTypes.has(fieldType)) {
      return;
    }

    const options = Array.isArray(optionsJson) ? optionsJson.map(String) : fieldType === QuestionFieldType.YES_NO ? ['Yes', 'No'] : [];
    if (fieldType === QuestionFieldType.MULTI_SELECT || fieldType === QuestionFieldType.CHECKBOX) {
      if (!Array.isArray(answerJson)) return;
      if (answerJson.some((item) => !options.includes(String(item)))) {
        throw new ConflictException('Submitted answer contains invalid option values');
      }
      return;
    }

    if (answerJson !== undefined && answerJson !== null && !options.includes(String(answerJson))) {
      throw new ConflictException('Submitted answer contains invalid option value');
    }
  }

  private async persistSubmission(
    assignment: Awaited<ReturnType<SubmissionsService['loadAssignmentForUser']>>,
    dto: SaveSubmissionDto,
    user: JwtAuthUser,
    isFinal: boolean,
  ) {
    const existing = assignment.submissions[0];
    const submission = existing
      ? await this.prisma.assessmentSubmission.update({
          where: { id: existing.id },
          data: {
            finalStatus: isFinal ? SubmissionFinalStatus.SUBMITTED : SubmissionFinalStatus.DRAFT,
            submittedBy: isFinal ? user.sub : null,
            submittedAt: isFinal ? new Date() : null,
            updatedBy: user.sub,
          },
        })
      : await this.prisma.assessmentSubmission.create({
          data: {
            assignmentId: assignment.id,
            finalStatus: isFinal ? SubmissionFinalStatus.SUBMITTED : SubmissionFinalStatus.DRAFT,
            submittedBy: isFinal ? user.sub : null,
            submittedAt: isFinal ? new Date() : null,
            createdBy: user.sub,
            updatedBy: user.sub,
          },
        });

    const incomingIds = dto.answers.map((answer) => answer.questionId);
    const existingAnswers = await this.prisma.assessmentAnswer.findMany({
      where: {
        submissionId: submission.id,
      },
    });
    const existingAnswersByQuestionId = new Map(
      existingAnswers.map((item) => [item.questionId, item]),
    );

    await this.prisma.$transaction([
      this.prisma.assessmentAnswer.updateMany({
        where: {
          submissionId: submission.id,
          deletedAt: null,
          questionId: { notIn: incomingIds },
        },
        data: {
          deletedAt: new Date(),
          deletedBy: user.sub,
          updatedBy: user.sub,
        },
      }),
      ...dto.answers.map((answer) => {
        const existingAnswer = existingAnswersByQuestionId.get(answer.questionId);
        return existingAnswer
          ? this.prisma.assessmentAnswer.update({
              where: { id: existingAnswer.id },
              data: {
                answerText: answer.answerText,
                answerJson: answer.answerJson as Prisma.InputJsonValue | undefined,
                deletedAt: null,
                deletedBy: null,
                updatedBy: user.sub,
              },
            })
          : this.prisma.assessmentAnswer.create({
              data: {
                submissionId: submission.id,
                questionId: answer.questionId,
                answerText: answer.answerText,
                answerJson: answer.answerJson as Prisma.InputJsonValue | undefined,
                createdBy: user.sub,
                updatedBy: user.sub,
              },
            });
      }),
      this.prisma.assessmentAssignment.update({
        where: { id: assignment.id },
        data: {
          status: isFinal ? AssignmentStatus.SUBMITTED : AssignmentStatus.DRAFT,
          updatedBy: user.sub,
        },
      }),
    ]);

    const result = await this.prisma.assessmentSubmission.findUnique({
      where: { id: submission.id },
      include: {
        answers: {
          where: { deletedAt: null },
          orderBy: { updatedAt: 'asc' },
        },
      },
    });

    await this.auditService.log({
      module: 'submissions',
      action: isFinal ? AuditAction.SUBMIT : AuditAction.UPDATE,
      entityType: AuditEntityType.ASSIGNMENT,
      entityId: assignment.id,
      remarks: isFinal ? 'Final submission completed' : 'Draft saved',
      newValues: { answerCount: dto.answers.length } as Prisma.InputJsonValue,
      createdBy: user.sub,
      updatedBy: user.sub,
    });

    return result;
  }
}
