import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ModuleVersion } from './module-version.entity';
import { Module as ModuleEntity } from '../modules/module.entity';
import { Lesson } from '../lessons/lesson.entity';
import { Activity } from '../activities/activity.entity';
import { ContentBlock } from '../content-blocks/content-block.entity';

export type ContentStatus = 'DRAFT' | 'IN_REVIEW' | 'PUBLISHED' | 'ARCHIVED';

/** Transitions the publishing workflow allows (section 10). */
const ALLOWED_TRANSITIONS: Record<ContentStatus, ContentStatus[]> = {
  DRAFT: ['IN_REVIEW', 'PUBLISHED', 'ARCHIVED'],
  IN_REVIEW: ['DRAFT', 'PUBLISHED', 'ARCHIVED'],
  PUBLISHED: ['DRAFT', 'ARCHIVED'],
  ARCHIVED: ['DRAFT'],
};

export interface ModuleSnapshot {
  module: Record<string, any>;
  lessons: Record<string, any>[];
}

@Injectable()
export class ModuleVersionsService {
  constructor(
    @InjectRepository(ModuleVersion)
    private readonly versionsRepo: Repository<ModuleVersion>,
    @InjectRepository(ModuleEntity)
    private readonly modulesRepo: Repository<ModuleEntity>,
    @InjectRepository(Lesson)
    private readonly lessonsRepo: Repository<Lesson>,
    @InjectRepository(Activity)
    private readonly activitiesRepo: Repository<Activity>,
    @InjectRepository(ContentBlock)
    private readonly blocksRepo: Repository<ContentBlock>,
  ) {}

  assertTransition(from: string, to: ContentStatus): void {
    const allowed = ALLOWED_TRANSITIONS[from as ContentStatus];
    if (!allowed || !allowed.includes(to)) {
      throw new BadRequestException(`Cannot move content from ${from} to ${to}`);
    }
  }

  /**
   * Captures the complete module tree so a published version stays readable
   * even after the working copy is edited (section 11).
   */
  async snapshot(moduleId: number): Promise<ModuleSnapshot> {
    const module = await this.modulesRepo.findOne({ where: { id: moduleId } });
    if (!module) throw new NotFoundException('Module not found');

    const lessons = await this.lessonsRepo.find({
      where: { moduleId },
      order: { orderIndex: 'ASC' },
    });

    const lessonSnapshots = await Promise.all(
      lessons.map(async (lesson) => {
        const activities = await this.activitiesRepo.find({
          where: { lessonId: lesson.id },
          order: { orderIndex: 'ASC' },
        });

        const activitySnapshots = await Promise.all(
          activities.map(async (activity) => ({
            ...activity,
            contentBlocks: await this.blocksRepo.find({
              where: { activityId: activity.id },
              order: { orderIndex: 'ASC' },
            }),
          })),
        );

        return { ...lesson, activities: activitySnapshots };
      }),
    );

    return { module: { ...module }, lessons: lessonSnapshots };
  }

  async createVersion(
    moduleId: number,
    options: { status?: ContentStatus; changeReason?: string; userId?: number } = {},
  ): Promise<ModuleVersion> {
    const snapshot = await this.snapshot(moduleId);
    const module = snapshot.module;

    const version = this.versionsRepo.create({
      moduleId,
      versionNumber: await this.nextVersionNumber(moduleId),
      title: module.title,
      description: module.description,
      data: { ...snapshot, changeReason: options.changeReason },
      status: options.status || 'DRAFT',
      createdBy: options.userId,
    });

    if (version.status === 'PUBLISHED') {
      version.publishedBy = options.userId;
      version.publishedAt = new Date();
    }

    return this.versionsRepo.save(version);
  }

  /** Snapshots the module, marks the snapshot published and points the module at it. */
  async publish(moduleId: number, userId?: number, changeReason?: string) {
    const module = await this.modulesRepo.findOne({ where: { id: moduleId } });
    if (!module) throw new NotFoundException('Module not found');
    this.assertTransition(module.status, 'PUBLISHED');

    const version = await this.createVersion(moduleId, {
      status: 'PUBLISHED',
      changeReason,
      userId,
    });

    // Supersede whatever was previously live.
    await this.versionsRepo
      .createQueryBuilder()
      .update(ModuleVersion)
      .set({ status: 'ARCHIVED' })
      .where('module_id = :moduleId', { moduleId })
      .andWhere('id != :id', { id: version.id })
      .andWhere('status = :status', { status: 'PUBLISHED' })
      .execute();

    await this.modulesRepo.update(moduleId, {
      status: 'PUBLISHED',
      currentVersionId: version.id,
    });

    return { module: await this.modulesRepo.findOne({ where: { id: moduleId } }), version };
  }

  async setStatus(moduleId: number, status: ContentStatus, userId?: number) {
    const module = await this.modulesRepo.findOne({ where: { id: moduleId } });
    if (!module) throw new NotFoundException('Module not found');
    this.assertTransition(module.status, status);

    if (status === 'PUBLISHED') return this.publish(moduleId, userId);

    await this.modulesRepo.update(moduleId, { status });
    return { module: await this.modulesRepo.findOne({ where: { id: moduleId } }), version: null };
  }

  findByModule(moduleId: number): Promise<ModuleVersion[]> {
    return this.versionsRepo.find({
      where: { moduleId },
      relations: ['creator', 'publisher'],
      order: { versionNumber: 'DESC' },
    });
  }

  findById(id: number): Promise<ModuleVersion | null> {
    return this.versionsRepo.findOne({
      where: { id },
      relations: ['creator', 'publisher'],
    });
  }

  findPublished(moduleId: number): Promise<ModuleVersion | null> {
    return this.versionsRepo.findOne({
      where: { moduleId, status: 'PUBLISHED' },
      order: { versionNumber: 'DESC' },
    });
  }

  private async nextVersionNumber(moduleId: number): Promise<number> {
    const latest = await this.versionsRepo.findOne({
      where: { moduleId },
      order: { versionNumber: 'DESC' },
    });
    return latest ? latest.versionNumber + 1 : 1;
  }
}
