import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { Injectable } from "@nestjs/common";
import { db } from "../config/database.js";
import { exams } from "./exams.schema.js";
import { examsRepository } from "./exams.repository.js";
import { outboxRepository } from "../shared/outbox/outbox.repository.js";
import { buildEvent, serializeEvent } from "../shared/outbox/event-builder.js";
import {
  canTransition,
  transition,
  type ExamStatus,
  type ExamAction,
} from "./exam-state-machine.js";
import type { Exam, NewExam } from "./exams.schema.js";
import {
  NotFoundError,
  ValidationError,
  ConflictError,
  ApplicationError,
  CoreEduErrorCode,
} from "../shared/errors/application-error.js";

export interface CreateExamInput {
  classId: string;
  subjectId: string;
  title: string;
  description?: string;
  examDate: Date | string;
  duration: number;
  totalScore: string;
  schoolId: string;
  createdBy: string;
}

export interface UpdateExamInput {
  title?: string;
  description?: string;
  examDate?: Date;
  duration?: number;
  totalScore?: string;
}

export interface AnswerInput {
  questionId: string;
  answer: string;
}

export interface ScoreInput {
  questionId: string;
  score: string;
  teacherComment?: string;
}

@Injectable()
export class ExamsService {
  async createExam(
    input: CreateExamInput,
    userId?: string,
  ): Promise<{ id: string }> {
    if (
      !input.classId ||
      !input.title ||
      !input.createdBy ||
      !input.subjectId
    ) {
      throw new ValidationError(
        "classId, subjectId, title, createdBy are required",
      );
    }

    const id = randomUUID();
    const exam: NewExam = {
      id,
      classId: input.classId,
      subjectId: input.subjectId,
      title: input.title,
      description: input.description,
      examDate:
        input.examDate instanceof Date
          ? input.examDate
          : new Date(input.examDate),
      duration: input.duration,
      totalScore: input.totalScore,
      status: "draft",
      statusChangedAt: new Date(),
      schoolId: input.schoolId,
      createdBy: input.createdBy,
    };

    const event = buildEvent({
      aggregateId: id,
      eventType: "exam.created",
      payload: {
        examId: id,
        classId: input.classId,
        subjectId: input.subjectId,
        title: input.title,
      },
      userId,
    });

    await db.transaction(async (tx) => {
      await tx.insert(exams).values(exam);
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: id,
          aggregateType: "exam",
          eventType: "exam.created",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });

    return { id };
  }

  async getExam(id: string): Promise<Exam> {
    const exam = await examsRepository.findById(id);
    if (!exam) {
      throw new NotFoundError(`Exam ${id} not found`);
    }
    return exam;
  }

  async listExamsByClass(classId: string): Promise<Exam[]> {
    return examsRepository.findByClassId(classId);
  }

  async updateExam(id: string, data: UpdateExamInput): Promise<void> {
    const existing = await examsRepository.findById(id);
    if (!existing) {
      throw new NotFoundError(`Exam ${id} not found`);
    }
    if (existing.status !== "draft") {
      throw new ConflictError(
        `Cannot update exam in status ${existing.status} (only draft allows edits)`,
      );
    }

    await db.transaction(async (tx) => {
      await tx.update(exams).set(data).where(eq(exams.id, id));
      const event = buildEvent({
        aggregateId: id,
        eventType: "exam.updated",
        payload: { examId: id, changes: data },
      });
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: id,
          aggregateType: "exam",
          eventType: "exam.updated",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });
  }

  async deleteExam(id: string): Promise<void> {
    const existing = await examsRepository.findById(id);
    if (!existing) {
      throw new NotFoundError(`Exam ${id} not found`);
    }

    await db.transaction(async (tx) => {
      await tx.delete(exams).where(eq(exams.id, id));
      const event = buildEvent({
        aggregateId: id,
        eventType: "exam.deleted",
        payload: { examId: id },
      });
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: id,
          aggregateType: "exam",
          eventType: "exam.deleted",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });
  }

  async publishExam(id: string, publishedBy: string): Promise<void> {
    const exam = await examsRepository.findById(id);
    if (!exam) {
      throw new NotFoundError(`Exam ${id} not found`);
    }
    const currentStatus = exam.status as ExamStatus;
    this.assertTransition(currentStatus, "publish");
    const newStatus = transition(currentStatus, "publish");

    await db.transaction(async (tx) => {
      await tx
        .update(exams)
        .set({
          status: newStatus,
          statusChangedAt: new Date(),
          statusChangedBy: publishedBy,
        })
        .where(eq(exams.id, id));
      const event = buildEvent({
        aggregateId: id,
        eventType: "exam.published",
        payload: {
          examId: id,
          classId: exam.classId,
          subjectId: exam.subjectId,
        },
        userId: publishedBy,
      });
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: id,
          aggregateType: "exam",
          eventType: "exam.published",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });
  }

  async submitExam(
    examId: string,
    studentId: string,
    _answers: AnswerInput[],
  ): Promise<{ submissionId: string }> {
    const exam = await examsRepository.findById(examId);
    if (!exam) {
      throw new NotFoundError(`Exam ${examId} not found`);
    }

    // Check existing submission (idempotency via unique index)
    const existing = await examsRepository.findSubmission(examId, studentId);
    if (
      existing &&
      (existing.status === "submitted" || existing.status === "graded")
    ) {
      throw new ConflictError(
        `Student ${studentId} already submitted exam ${examId}`,
      );
    }

    const submissionId = randomUUID();
    await db.transaction(async (tx) => {
      await examsRepository.createSubmission(
        {
          id: submissionId,
          examId,
          studentId,
          status: "submitted",
          submittedAt: new Date(),
        },
        tx,
      );

      // ISSUE-006 决策 #6: exam.submitted 事件只带 submission_id
      const event = buildEvent({
        aggregateId: examId,
        eventType: "exam.submitted",
        payload: {
          examId,
          submissionId,
          studentId,
        },
        userId: studentId,
      });
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: examId,
          aggregateType: "exam",
          eventType: "exam.submitted",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });

    return { submissionId };
  }

  async gradeExam(
    examId: string,
    submissionId: string,
    scores: ScoreInput[],
    gradedBy: string,
  ): Promise<{ totalScore: string }> {
    const exam = await examsRepository.findById(examId);
    if (!exam) {
      throw new NotFoundError(`Exam ${examId} not found`);
    }
    const submission = await examsRepository.findSubmissionById(submissionId);
    if (!submission || submission.examId !== examId) {
      throw new NotFoundError(
        `Submission ${submissionId} not found for exam ${examId}`,
      );
    }
    if (submission.status === "graded") {
      throw new ConflictError(`Submission ${submissionId} already graded`);
    }

    // Calculate total score
    const totalScore = scores.reduce((sum, s) => sum + Number(s.score), 0);
    const totalScoreStr = totalScore.toFixed(2);

    await db.transaction(async (tx) => {
      await examsRepository.updateSubmission(submissionId, {
        status: "graded",
        gradedAt: new Date(),
        gradedBy,
        totalScore: totalScore.toFixed(2),
      });

      const event = buildEvent({
        aggregateId: examId,
        eventType: "exam.graded",
        payload: {
          examId,
          submissionId,
          studentId: submission.studentId,
          totalScore: totalScoreStr,
        },
        userId: gradedBy,
      });
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: examId,
          aggregateType: "exam",
          eventType: "exam.graded",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });

    return { totalScore: totalScoreStr };
  }

  async archiveExam(id: string, archivedBy: string): Promise<void> {
    const exam = await examsRepository.findById(id);
    if (!exam) {
      throw new NotFoundError(`Exam ${id} not found`);
    }
    // ISSUE-006 决策 #7: archived 仅做软删除
    if (exam.status === "archived") {
      return; // Already archived
    }
    if (exam.status !== "graded") {
      throw new ConflictError(
        `Cannot archive exam in status ${exam.status} (only graded allows archive)`,
      );
    }

    await db.transaction(async (tx) => {
      await tx
        .update(exams)
        .set({
          status: "archived",
          statusChangedAt: new Date(),
          statusChangedBy: archivedBy,
          archivedAt: new Date(),
        })
        .where(eq(exams.id, id));
    });
  }

  private assertTransition(from: ExamStatus, action: ExamAction): void {
    if (!canTransition(from, action)) {
      throw new ApplicationError(
        CoreEduErrorCode.EXAM_INVALID_TRANSITION,
        `Invalid exam state transition: ${from} --${action}-->`,
        409,
        { from, action },
      );
    }
  }
}
