import {
  BoardRepository,
  type ListThreadsOptions,
} from '@/repositories/BoardRepository';
import type { Paginated } from '@/repositories/BoardRepository';
import { ProjectMemberRepository } from '@/repositories/ProjectMemberRepository';
import { NotificationService } from '@/services/NotificationService';
import { ActivityLogService } from '@/services/ActivityLogService';
import { AttachmentService } from '@/services/AttachmentService';
import type { SqliteDatabase } from '@/lib/db/sqlite';
import { ForbiddenError, NotFoundError, ValidationError } from '@/lib/errors';
import type {
  AttachmentView,
  BoardCategory,
  BoardComment,
  BoardThread,
} from '@/lib/types';

const VALID_CATEGORIES: BoardCategory[] = [
  'notice',
  'spec',
  'minutes',
  'question',
  'decision',
  'trouble',
  'memo',
];

export interface CreateThreadInput {
  title: string;
  bodyMd: string;
  category?: BoardCategory;
  fileIds?: number[];
}

export interface UpdateThreadInput {
  title?: string;
  bodyMd?: string;
  category?: BoardCategory;
  isPinned?: number;
  isImportant?: number;
}

/**
 * 掲示板の業務ロジックを担うService。
 * 権限チェック(プロジェクト参加者のみ)、スレッド/コメントCRUD、
 * コメント追加時の通知(board_commented)、アクティビティログ記録、
 * 添付ファイル紐付けを行う。
 */
export class BoardService {
  constructor(
    private readonly boardRepository: BoardRepository,
    private readonly projectMemberRepository: ProjectMemberRepository,
    private readonly notificationService: NotificationService,
    private readonly activityLogService: ActivityLogService,
    private readonly attachmentService: AttachmentService,
    private readonly db: SqliteDatabase
  ) {}

  listThreads(
    actorId: number,
    projectId: number,
    opts: ListThreadsOptions = {}
  ): Paginated<BoardThread> {
    this.requireMember(projectId, actorId);
    return this.boardRepository.findThreads(projectId, opts);
  }

  getThread(actorId: number, threadId: number): BoardThread {
    const thread = this.boardRepository.findThreadById(threadId);
    if (!thread) throw new NotFoundError('BoardThread', threadId);
    this.requireMember(thread.projectId, actorId);
    return thread;
  }

  createThread(
    actorId: number,
    projectId: number,
    input: CreateThreadInput
  ): BoardThread {
    this.requireMember(projectId, actorId);
    this.validateThreadInput(input.title, input.bodyMd, input.category);
    const thread = this.db.transaction(() => {
      const t = this.boardRepository.createThread({
        projectId,
        title: input.title,
        bodyMd: input.bodyMd,
        authorId: actorId,
        category: input.category ?? null,
      });
      if (input.fileIds && input.fileIds.length > 0) {
        this.attachmentService.attach(
          actorId,
          projectId,
          'board_thread',
          t.id,
          input.fileIds
        );
      }
      return t;
    });
    this.activityLogService.logActivity({
      projectId,
      actorId,
      action: 'board_posted',
      targetType: 'thread',
      targetId: thread.id,
    });
    return thread;
  }

  updateThread(
    actorId: number,
    threadId: number,
    input: UpdateThreadInput
  ): BoardThread {
    const thread = this.getThread(actorId, threadId);
    this.requireAuthorOrAdmin(thread.projectId, actorId, thread.authorId);
    if (
      input.title !== undefined ||
      input.bodyMd !== undefined ||
      input.category !== undefined
    ) {
      this.validateThreadInput(
        input.title ?? thread.title,
        input.bodyMd ?? thread.bodyMd,
        input.category
      );
    }
    const updated = this.boardRepository.updateThread(threadId, {
      title: input.title,
      bodyMd: input.bodyMd,
      category: input.category,
      isPinned: input.isPinned,
      isImportant: input.isImportant,
    });
    if (!updated) throw new NotFoundError('BoardThread', threadId);
    return updated;
  }

  deleteThread(actorId: number, threadId: number): void {
    const thread = this.getThread(actorId, threadId);
    this.requireAuthorOrAdmin(thread.projectId, actorId, thread.authorId);
    this.db.transaction(() => {
      this.attachmentService.detach('board_thread', threadId);
      this.boardRepository.deleteThread(threadId);
    });
  }

  listComments(
    actorId: number,
    threadId: number,
    page: number = 1
  ): Paginated<BoardComment> {
    const thread = this.getThread(actorId, threadId);
    return this.boardRepository.findCommentsByThread(thread.id, page);
  }

  createComment(
    actorId: number,
    threadId: number,
    bodyMd: string,
    fileIds?: number[]
  ): BoardComment {
    const thread = this.getThread(actorId, threadId);
    if (!bodyMd.trim()) {
      throw new ValidationError('コメント本文を入力してください', 'bodyMd');
    }
    const comment = this.db.transaction(() => {
      const c = this.boardRepository.createComment({
        threadId: thread.id,
        authorId: actorId,
        bodyMd,
      });
      if (fileIds && fileIds.length > 0) {
        this.attachmentService.attach(
          actorId,
          thread.projectId,
          'board_comment',
          c.id,
          fileIds
        );
      }
      return c;
    });
    // スレッド投稿者へ通知(自分自身へのコメントは通知しない)
    if (thread.authorId !== actorId) {
      this.notificationService.notifyOnEvent({
        type: 'board_commented',
        projectId: thread.projectId,
        title: `スレッド「${thread.title}」にコメントがつきました`,
        body: bodyMd.slice(0, 100),
        threadAuthorId: thread.authorId,
      });
    }
    this.activityLogService.logActivity({
      projectId: thread.projectId,
      actorId,
      action: 'comment_added',
      targetType: 'comment',
      targetId: comment.id,
    });
    return comment;
  }

  updateComment(
    actorId: number,
    commentId: number,
    bodyMd: string
  ): BoardComment {
    const comment = this.boardRepository.findCommentById(commentId);
    if (!comment) throw new NotFoundError('BoardComment', commentId);
    this.getThread(actorId, comment.threadId);
    if (comment.authorId !== actorId) {
      throw new ForbiddenError('自分のコメントのみ編集できます');
    }
    if (!bodyMd.trim()) {
      throw new ValidationError('コメント本文を入力してください', 'bodyMd');
    }
    const updated = this.boardRepository.updateComment(commentId, bodyMd);
    if (!updated) throw new NotFoundError('BoardComment', commentId);
    return updated;
  }

  deleteComment(actorId: number, commentId: number): void {
    const comment = this.boardRepository.findCommentById(commentId);
    if (!comment) throw new NotFoundError('BoardComment', commentId);
    const thread = this.getThread(actorId, comment.threadId);
    this.requireAuthorOrAdmin(thread.projectId, actorId, comment.authorId);
    this.db.transaction(() => {
      this.attachmentService.detach('board_comment', commentId);
      this.boardRepository.deleteComment(commentId);
    });
  }

  /**
   * スレッド本文と指定コメントの添付ファイルを一括取得する。
   * メンバーシップは getThread で検証する。
   */
  getAttachments(
    actorId: number,
    threadId: number,
    commentIds: number[]
  ): { thread: AttachmentView[]; comments: AttachmentView[] } {
    const thread = this.getThread(actorId, threadId);
    return {
      thread: this.attachmentService.listViews('board_thread', thread.id),
      comments: this.attachmentService.listViewsBatch(
        'board_comment',
        commentIds
      ),
    };
  }

  private requireMember(projectId: number, actorId: number): void {
    if (!this.projectMemberRepository.isMember(projectId, actorId)) {
      throw new ForbiddenError('プロジェクトに参加していません');
    }
  }

  private requireAuthorOrAdmin(
    projectId: number,
    actorId: number,
    authorId: number
  ): void {
    if (actorId === authorId) return;
    const role = this.projectMemberRepository.getRole(projectId, actorId);
    if (role !== 'admin') {
      throw new ForbiddenError(
        '投稿者またはプロジェクト管理者のみ操作できます'
      );
    }
  }

  private validateThreadInput(
    title: string,
    bodyMd: string,
    category?: BoardCategory
  ): void {
    if (!title.trim()) {
      throw new ValidationError('タイトルを入力してください', 'title');
    }
    if (title.length > 200) {
      throw new ValidationError(
        'タイトルは200文字以内で入力してください',
        'title'
      );
    }
    if (!bodyMd.trim()) {
      throw new ValidationError('本文を入力してください', 'bodyMd');
    }
    if (category && !VALID_CATEGORIES.includes(category)) {
      throw new ValidationError('無効なカテゴリです', 'category');
    }
  }
}
