import { randomUUID } from "node:crypto";
import { Injectable } from "@nestjs/common";
import { db } from "../config/database.js";
import { homework } from "./homework.schema.js";
import { homeworkRepository } from "./homework.repository.js";
import { outboxRepository } from "../shared/outbox/outbox.repository.js";
import { buildEvent, serializeEvent } from "../shared/outbox/event-builder.js";
import { acquireLock, releaseLock } from "../config/redis.js";
import {
  NotFoundError,
  ValidationError,
  ConflictError,
} from "../shared/errors/application-error.js";
import type { Homework, NewHomework } from "./homework.schema.js";

export interface AssignHomeworkInput {
  classId: string;
  subjectId: string;
  title: string;
  description?: string;
  dueDate: Date | string;
  gracePeriod?: number;
  schoolId: string;
  createdBy: string;
}

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

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

const LOCK_TTL_SECONDS = 30;

@Injectable()
export class HomeworkService {
  async assignHomework(
    input: AssignHomeworkInput,
    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 record: NewHomework = {
      id,
      classId: input.classId,
      subjectId: input.subjectId,
      title: input.title,
      description: input.description,
      dueDate:
        input.dueDate instanceof Date ? input.dueDate : new Date(input.dueDate),
      gracePeriod: input.gracePeriod ?? 300,
      status: "assigned",
      schoolId: input.schoolId,
      createdBy: input.createdBy,
    };

    const event = buildEvent({
      aggregateId: id,
      eventType: "homework.assigned",
      payload: {
        homeworkId: id,
        classId: input.classId,
        subjectId: input.subjectId,
        title: input.title,
      },
      userId,
    });

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

    return { id };
  }

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

  async listByClass(classId: string): Promise<Homework[]> {
    return homeworkRepository.findByClassId(classId);
  }

  async submitHomework(
    homeworkId: string,
    studentId: string,
    answers: AnswerInput[],
  ): Promise<{ submissionId: string }> {
    const hw = await homeworkRepository.findById(homeworkId);
    if (!hw) {
      throw new NotFoundError(`Homework ${homeworkId} not found`);
    }

    // Redis distributed lock for idempotency (P2 feature)
    const lockKey = `hw:submit:${homeworkId}:${studentId}`;
    const locked = await acquireLock(lockKey, LOCK_TTL_SECONDS);
    if (!locked) {
      // Lock unavailable - check if submission already exists
      const existing = await homeworkRepository.findSubmission(
        homeworkId,
        studentId,
      );
      if (
        existing &&
        (existing.status === "submitted" || existing.status === "graded")
      ) {
        throw new ConflictError(
          `Student ${studentId} already submitted homework ${homeworkId}`,
        );
      }
      throw new ConflictError("Submission in progress, please retry");
    }

    try {
      // Double-check after acquiring lock
      const existing = await homeworkRepository.findSubmission(
        homeworkId,
        studentId,
      );
      if (
        existing &&
        (existing.status === "submitted" || existing.status === "graded")
      ) {
        throw new ConflictError(
          `Student ${studentId} already submitted homework ${homeworkId}`,
        );
      }

      const submissionId = randomUUID();
      const now = new Date();

      // Check grace period
      const dueWithGrace = new Date(
        hw.dueDate.getTime() + hw.gracePeriod * 1000,
      );
      const isLate = now > dueWithGrace;

      await db.transaction(async (tx) => {
        await homeworkRepository.createSubmission(
          {
            id: submissionId,
            homeworkId,
            studentId,
            status: "submitted",
            submittedAt: now,
          },
          tx,
        );

        // Insert answers
        if (answers.length > 0) {
          const answerRecords = answers.map((a) => ({
            id: randomUUID(),
            submissionId,
            questionId: a.questionId,
            answer: a.answer,
          }));
          await homeworkRepository.createAnswers(answerRecords, tx);
        }

        const event = buildEvent({
          aggregateId: homeworkId,
          eventType: "homework.submitted",
          payload: {
            homeworkId,
            submissionId,
            studentId,
            isLate,
          },
          userId: studentId,
        });
        await outboxRepository.create(
          {
            id: randomUUID(),
            eventId: event.event_id,
            aggregateId: homeworkId,
            aggregateType: "homework",
            eventType: "homework.submitted",
            occurredAt: new Date(event.occurred_at),
            payload: serializeEvent(event),
            status: "pending",
          },
          tx,
        );
      });

      return { submissionId };
    } finally {
      await releaseLock(lockKey);
    }
  }

  async gradeHomework(
    homeworkId: string,
    submissionId: string,
    scores: ScoreInput[],
    feedback: string | undefined,
    gradedBy: string,
  ): Promise<{ totalScore: string }> {
    const hw = await homeworkRepository.findById(homeworkId);
    if (!hw) {
      throw new NotFoundError(`Homework ${homeworkId} not found`);
    }
    const submission =
      await homeworkRepository.findSubmissionById(submissionId);
    if (!submission || submission.homeworkId !== homeworkId) {
      throw new NotFoundError(
        `Submission ${submissionId} not found for homework ${homeworkId}`,
      );
    }
    if (submission.status === "graded") {
      throw new ConflictError(`Submission ${submissionId} already graded`);
    }

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

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

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

    return { totalScore: totalScoreStr };
  }
}
