// aislop-ignore-file: duplicate-block -- 导入校验循环结构相似，逻辑已复用现有助手
import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, In, Repository } from 'typeorm';
import { uuidV7 } from '../common/uuid-v7';
import { AiReview } from './entities/ai-review.entity';
import type {
  AiReviewSection,
  AiReviewSectionType,
} from './entities/ai-review.entity';
import type { ExcelSheetRows } from './ai-excel-reader.service';
import {
  AiReviewStepSubmitResult,
  AiReviewSubmitResult,
  MAX_SECTIONS_JSON_BYTES,
  withInitialSectionState,
} from './ai-review.shared';
import { buildSectionsFromWorkbookAsync, parseSections } from './ai-review.workbook';
import { validateSchema } from './ai-review.validation';
import { enrichWithIssues } from './ai-review.enrich';
import {
  submitAll,
  submitGroup,
  submitSection,
} from './ai-review.submit';
import type { AiReviewSubmitContext } from './ai-review.submit';

/**
 * A2UI batch-import review lifecycle.
 *
 * The model parses an uploaded workbook (students / rooms / transfers),
 * calls `render_review`, and the user inspects per-table preview cards
 * before confirming. Confirmation runs each section in its own
 * transaction in dependency order: students → rooms → transfers →
 * checkins. Per-row problems are collected as issues and the row is
 * skipped instead of failing the whole import.
 */
@Injectable()
export class AiReviewService {
  constructor(
    @InjectRepository(AiReview)
    private readonly reviews: Repository<AiReview>,
    private readonly dataSource: DataSource,
  ) {}

  private get submitContext(): AiReviewSubmitContext {
    return { reviews: this.reviews, dataSource: this.dataSource };
  }

  /**
   * Validate `render_review` arguments and persist a pending review.
   * Throws BadRequestException when the schema is unsafe/invalid.
   */
  async createReview(
    input: { userId: number; conversationId: number; assistantMessageId: number },
    rawArgs: unknown,
  ): Promise<AiReview> {
    const schema = validateSchema(rawArgs);
    const sections = (await enrichWithIssues(this.dataSource, schema.sections)).map(
      withInitialSectionState,
    );
    const sectionsJson = JSON.stringify(sections);
    if (Buffer.byteLength(sectionsJson, 'utf8') > MAX_SECTIONS_JSON_BYTES) {
      throw new BadRequestException('预览数据过大');
    }
    return this.reviews.save(
      this.reviews.create({
        id: uuidV7(),
        userId: input.userId,
        conversationId: input.conversationId,
        assistantMessageId: input.assistantMessageId,
        title: schema.title,
        summary: schema.summary,
        sectionsJson,
        status: 'pending',
        resultSummary: null,
        submittedAt: null,
      }),
    );
  }

  async findOwnedPending(reviewId: string, userId: number): Promise<AiReview> {
    const review = await this.reviews.findOne({
      where: { id: reviewId, userId, status: 'pending' },
    });
    if (!review) throw new NotFoundException('导入预览不存在、已确认或已失效');
    return review;
  }

  /**
   * Mark every other pending review in the same conversation as expired.
   * Called after a new render_review is successfully created so older cards
   * are superseded instead of silently staying confirmable.
   */
  async expirePreviousReviews(
    userId: number,
    conversationId: number,
    exceptReviewId: string,
  ): Promise<AiReview[]> {
    const pending = await this.reviews.find({
      where: { userId, conversationId, status: 'pending' },
    });
    const expired = pending.filter((review) => review.id !== exceptReviewId);
    if (expired.length === 0) return [];
    const ids = expired.map((review) => review.id);
    await this.reviews.update({ id: In(ids) }, { status: 'expired' });
    return expired.map((review) => ({ ...review, status: 'expired' as const }));
  }

  /**
   * Return a review owned by the user regardless of overall status.
   * Used by step confirmation so an already-completed card can respond
   * with a conflict instead of a plain not-found error.
   */
  async findOwned(reviewId: string, userId: number): Promise<AiReview> {
    const review = await this.reviews.findOne({
      where: { id: reviewId, userId },
    });
    if (!review) throw new NotFoundException('导入预览不存在');
    return review;
  }

  /**
   * Return the newest pending review bound to an assistant message, if any.
   * Used to guarantee at most one batch-import preview card per message.
   */
  async findPendingByAssistantMessage(assistantMessageId: number): Promise<AiReview | null> {
    return this.reviews.findOne({
      where: { assistantMessageId, status: 'pending' },
      order: { createdAt: 'DESC' },
    });
  }

  /**
   * 服务端直接解析上传的 Excel 生成审阅分表：行数据来自文件原文，
   * 不经过模型转抄，避免漏行/错值。表头按内置字典自动映射，
   * 模型可通过 sections[].columns[].sourceHeader 显式指定映射。
   */
  async buildSectionsFromWorkbook(
    sheets: ExcelSheetRows[],
    rawArgs: unknown,
  ): Promise<AiReviewSection[]> {
    return await buildSectionsFromWorkbookAsync(sheets, rawArgs);
  }

  /** Public shape sent via `ui.review` SSE and mirrored into message metadata. */
  serialize(review: AiReview): Record<string, unknown> {
    return {
      id: review.id,
      title: review.title,
      summary: review.summary,
      sections: this.parseSections(review.sectionsJson),
      status: review.status,
      resultSummary: review.resultSummary,
    };
  }

  parseSections(sectionsJson: string): AiReviewSection[] {
    return parseSections(sectionsJson);
  }

  async submitSection(
    reviewId: string,
    userId: number,
    sectionKey: string,
  ): Promise<AiReviewStepSubmitResult> {
    return submitSection(this.submitContext, reviewId, userId, sectionKey);
  }

  async submitAll(reviewId: string, userId: number): Promise<{
    review: AiReview;
    result: AiReviewSubmitResult;
  }> {
    return submitAll(this.submitContext, reviewId, userId);
  }

  async submitGroup(
    reviewId: string,
    userId: number,
    type: AiReviewSectionType,
  ): Promise<{ review: AiReview }> {
    return submitGroup(this.submitContext, reviewId, userId, type);
  }
}
