import { randomUUID } from "node:crypto";
import { Injectable } from "@nestjs/common";
import { db } from "../config/database.js";
import { gradesRepository } from "./grades.repository.js";
import { outboxRepository } from "../shared/outbox/outbox.repository.js";
import { buildEvent, serializeEvent } from "../shared/outbox/event-builder.js";
import {
  NotFoundError,
  ValidationError,
} from "../shared/errors/application-error.js";
import type { Grade, NewGrade } from "./grades.schema.js";

export interface RecordGradeInput {
  studentId: string;
  examId?: string;
  homeworkId?: string;
  score: string;
  totalScore: string;
  feedback?: string;
  gradedBy: string;
  schoolId: string;
  idempotencyKey?: string;
}

export interface UpdateGradeInput {
  score?: string;
  feedback?: string;
}

@Injectable()
export class GradesService {
  async recordGrade(
    input: RecordGradeInput,
    userId?: string,
  ): Promise<{ id: string }> {
    if (
      !input.studentId ||
      !input.score ||
      !input.gradedBy ||
      !input.schoolId
    ) {
      throw new ValidationError(
        "studentId, score, gradedBy, schoolId are required",
      );
    }
    if (!input.examId && !input.homeworkId) {
      throw new ValidationError("Either examId or homeworkId must be provided");
    }

    // Idempotency check
    if (input.idempotencyKey) {
      const existing = await gradesRepository.findByIdempotencyKey(
        input.idempotencyKey,
      );
      if (existing) {
        return { id: existing.id };
      }
    }

    const id = randomUUID();
    const record: NewGrade = {
      id,
      studentId: input.studentId,
      examId: input.examId,
      homeworkId: input.homeworkId,
      score: input.score,
      totalScore: input.totalScore,
      feedback: input.feedback,
      gradedBy: input.gradedBy,
      schoolId: input.schoolId,
      idempotencyKey: input.idempotencyKey,
    };

    const event = buildEvent({
      aggregateId: id,
      eventType: "grade.recorded",
      payload: {
        gradeId: id,
        studentId: input.studentId,
        score: input.score,
        totalScore: input.totalScore,
        examId: input.examId,
        homeworkId: input.homeworkId,
      },
      userId,
    });

    await db.transaction(async (tx) => {
      await gradesRepository.create(record, tx);
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: id,
          aggregateType: "grade",
          eventType: "grade.recorded",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });

    return { id };
  }

  async getGrade(id: string): Promise<Grade> {
    const record = await gradesRepository.findById(id);
    if (!record) {
      throw new NotFoundError(`Grade ${id} not found`);
    }
    return record;
  }

  async listByStudent(studentId: string): Promise<Grade[]> {
    return gradesRepository.findByStudentId(studentId);
  }

  async listByExam(examId: string): Promise<Grade[]> {
    return gradesRepository.findByExamId(examId);
  }

  async listByHomework(homeworkId: string): Promise<Grade[]> {
    return gradesRepository.findByHomeworkId(homeworkId);
  }

  async updateGrade(
    id: string,
    data: UpdateGradeInput,
    updatedBy: string,
  ): Promise<void> {
    const existing = await gradesRepository.findById(id);
    if (!existing) {
      throw new NotFoundError(`Grade ${id} not found`);
    }

    await db.transaction(async (tx) => {
      await gradesRepository.update(id, data);

      const event = buildEvent({
        aggregateId: id,
        eventType: "grade.updated",
        payload: {
          gradeId: id,
          studentId: existing.studentId,
          changes: data,
        },
        userId: updatedBy,
      });
      await outboxRepository.create(
        {
          id: randomUUID(),
          eventId: event.event_id,
          aggregateId: id,
          aggregateType: "grade",
          eventType: "grade.updated",
          occurredAt: new Date(event.occurred_at),
          payload: serializeEvent(event),
          status: "pending",
        },
        tx,
      );
    });
  }
}
