import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, IsNull, Repository } from 'typeorm';
import {
  AssetSelectorContext,
  AssetSelectorService,
} from './asset-selector.service';
import {
  CommentBlockDTO,
  CommentReplyChangeDTO,
  CommentCreateDTO,
  CommentCreateResponseDTO,
  CommentReplyDTO,
  GetCommentsResultDTO,
} from './dto/comment-dto';
import { ProjectAccess } from '../utils/project-access';
import { CommentEntity } from '../entities/comment.entity';
import { CommentBlockKeyEntity } from '../entities/comment_block_key.entity';
import { CommentReplyEntity } from '../entities/comment_reply.entity';
import { v4 as uuidv4 } from 'uuid';
import { AssetCommentDTO } from './dto/asset-dto';
import { UserDTO } from '../common/dto/user-dto';
import { ApiError } from '../common/error/api-error';
import { ApiErrorCodes } from '../common/error/api-error-codes';
import { AssetRights } from './logic/Rights';
import { assignRightsFilterInSelection } from './logic/assignRightsFilterInSelection';
import {
  AssetProps,
  parseAssetBlockKeyRef,
  stringifyAssetBlockRef,
} from './logic/Props';
import { getAssetBlockKeysTx } from './block-helpers';
import { decodeBigNumberKey } from '../utils/big-number-key';
import { ProjectUserEntity } from '../entities/project-user.entity';
import { UserEntity } from '../entities/user.entity';

@Injectable()
export class CommentService {
  constructor(
    @InjectRepository(CommentEntity)
    private readonly commentRepo: Repository<CommentEntity>,
    @InjectRepository(CommentBlockKeyEntity)
    private readonly commentBlockKeyRepo: Repository<CommentBlockKeyEntity>,
    @InjectRepository(CommentReplyEntity)
    private readonly commentReplyRepo: Repository<CommentReplyEntity>,
    private readonly assetSelectorService: AssetSelectorService,
  ) {}

  private async _checkUserIsAuthorAndReturnReply(
    reply_id: string,
    user: UserDTO,
  ) {
    // проверить что юзер автор комментария
    const comment_reply = await this.commentReplyRepo.findOne({
      id: reply_id,
      userId: user.id,
    });
    if (!comment_reply) {
      throw new ApiError(
        'Only author can change comment',
        ApiErrorCodes.ACTION_NOT_AVAILABLE,
      );
    }

    return comment_reply;
  }

  private async _checkRightToReply(
    comment_id: string,
    project_access: ProjectAccess,
  ) {
    const comment = await this.commentRepo
      .createQueryBuilder('c')
      .innerJoin(
        CommentBlockKeyEntity,
        'cb',
        'cb.project_id = c.project_id AND c.id = cb.comment_id',
      )
      .select(['DISTINCT cb.asset_id as asset_id'])
      .where('c.deletedAt IS NULL AND c.id = :commentId', {
        commentId: comment_id,
      })
      .execute();

    if (comment) {
      await this._checkRightToCommentForAsset(comment.asset_id, project_access);
    } else {
      throw new ApiError(
        'Comment does not exist',
        ApiErrorCodes.ENTITY_NOT_FOUND,
      );
    }
  }

  async _checkRightToCommentForAsset(
    asset_id: string,
    project_access: ProjectAccess,
  ) {
    //проверить право пользователя к комментарию и к ассету
    // получение ассета по id если возвращает доступ есть
    const existen_ids = await this.assetSelectorService.getIds(
      assignRightsFilterInSelection(
        {
          where: {
            id: asset_id,
            isSystem: false,
          },
        },
        AssetRights.COMMENT,
      ),
      project_access,
    );
    if (existen_ids.length === 0) {
      throw new ApiError(
        'You have not rights to comment',
        ApiErrorCodes.ACCESS_DENIED,
      );
    }
  }

  async createComment(
    params: CommentCreateDTO,
    projectAccess: ProjectAccess,
  ): Promise<CommentCreateResponseDTO> {
    await this._checkRightToCommentForAsset(params.assetId, projectAccess);

    const block_key_structures_with_anchors = params.blocks.map((b) => {
      return {
        ...parseAssetBlockKeyRef(b.ref),
        anchor: b.anchor,
      };
    });

    const block_keys = await getAssetBlockKeysTx(
      this.commentRepo.manager,
      projectAccess.projectId,
      block_key_structures_with_anchors,
    );

    if (block_keys.length === 0) {
      throw new ApiError(
        'Parameter "blocks" is empty',
        ApiErrorCodes.PARAM_EMPTY,
      );
    }

    const comment_id = uuidv4();
    const reply_id = uuidv4();

    await this.commentRepo.manager.transaction(async (tx) => {
      await tx.getRepository(CommentEntity).insert({
        id: comment_id,
        projectId: projectAccess.projectId,
      });
      await tx.getRepository(CommentReplyEntity).insert({
        id: reply_id,
        projectId: projectAccess.projectId,
        commentId: comment_id,
        userId: projectAccess.userId,
        content: params.content,
      });

      await tx.getRepository(CommentBlockKeyEntity).insert(
        block_keys.map((block) => {
          return {
            projectId: projectAccess.projectId,
            assetId: params.assetId,
            blockKey: block.blockKey,
            commentId: comment_id,
            anchor: block.anchor,
          };
        }),
      );
    });

    return {
      commentId: comment_id,
      replyId: reply_id,
    };
  }

  private async _checkCommentExists(comment_id: string, project_id: string) {
    const comment = await this.commentRepo.findOne({
      id: comment_id,
      projectId: project_id,
      deletedAt: IsNull(),
    });
    if (!comment) {
      throw new ApiError(
        'Comment does not exist',
        ApiErrorCodes.ENTITY_NOT_FOUND,
      );
    }
  }

  async addAnswer(
    params: CommentReplyChangeDTO,
    comment_id: string,
    projectAccess: ProjectAccess,
  ): Promise<CommentReplyDTO> {
    await this._checkRightToReply(comment_id, projectAccess);

    // если answerToId передан, проверить, что он относится к той же самой ветке комментария
    if (params.answerToReplyId) {
      const reply_message = await this.commentReplyRepo.findOne({
        id: params.answerToReplyId,
        projectId: projectAccess.projectId,
        commentId: comment_id,
      });
      if (!reply_message) {
        throw new ApiError(
          'Comment branch has not this comment',
          ApiErrorCodes.ENTITY_NOT_FOUND,
        );
      }
    }

    const reply_id = uuidv4();
    let comment: CommentReplyDTO;
    await this._checkCommentExists(comment_id, projectAccess.projectId);
    await this.commentRepo.manager.transaction(async (tx) => {
      await tx.getRepository(CommentReplyEntity).insert({
        id: reply_id,
        answerToId: params.answerToReplyId,
        projectId: projectAccess.projectId,
        commentId: comment_id,
        userId: projectAccess.userId,
        content: params.content,
      });

      await tx
        .getRepository(CommentEntity)
        .createQueryBuilder()
        .update(CommentEntity)
        .where({ id: comment_id, projectId: projectAccess.projectId })
        .set({ updatedAt: () => 'now()' })
        .execute();

      comment = await this._getCommentById(tx, reply_id);
    });

    return comment;
  }

  async changeAnswer(
    projectAccess: ProjectAccess,
    comment_id: string,
    old_reply_id: string,
    params: CommentReplyChangeDTO,
    user: UserDTO,
  ): Promise<CommentReplyDTO> {
    await this._checkRightToReply(comment_id, projectAccess);
    const old_reply = await this._checkUserIsAuthorAndReturnReply(
      old_reply_id,
      user,
    );
    const changed_reply_id = uuidv4();

    await this._checkCommentExists(comment_id, projectAccess.projectId);
    let comment: CommentReplyDTO;
    await this.commentRepo.manager.transaction(async (tx) => {
      // добавляю новый
      await tx.getRepository(CommentReplyEntity).insert({
        id: changed_reply_id,
        answerToId: old_reply.answerToId,
        projectId: old_reply.projectId,
        commentId: old_reply.commentId,
        userId: old_reply.userId,
        content: params.content,
        createdAt: old_reply.createdAt,
      });

      // меняю старый
      await tx
        .getRepository(CommentReplyEntity)
        .createQueryBuilder()
        .update(CommentReplyEntity)
        .where({
          id: old_reply_id,
          commentId: comment_id,
          projectId: projectAccess.projectId,
        })
        .set({ changedToId: changed_reply_id })
        .execute();

      await tx
        .getRepository(CommentEntity)
        .createQueryBuilder()
        .update(CommentEntity)
        .where({ id: comment_id, projectId: projectAccess.projectId })
        .set({ updatedAt: () => 'now()' })
        .execute();

      comment = await this._getCommentById(tx, changed_reply_id);
    });

    return comment;
  }

  private async _getCommentById(
    tx: EntityManager,
    comment_id: string,
  ): Promise<CommentReplyDTO> {
    const comment = await tx
      .getRepository(CommentReplyEntity)
      .createQueryBuilder('cr')
      .innerJoin(UserEntity, 'u', 'cr.user_id = u.id')
      .leftJoin(
        ProjectUserEntity,
        'pu',
        'pu.user_id = u.id AND pu.project_id = cr.project_id',
      )
      .select([
        'cr.id as id',
        'cr.comment_id as comment_id',
        'cr.answer_to_id as answer_to_id',
        'cr.user_id as user_id',
        'COALESCE(pu.user_name, u.name) as user_name',
        'cr.content as content',
        'cr.created_at as created_at',
        'cr.updated_at as updated_at',
      ])
      .where(
        'cr.id = :comment_id AND cr.deleted_at IS NULL AND cr.changed_to_id IS NULL',
        { comment_id },
      )
      .getRawOne<{
        id: string;
        comment_id: string;
        answer_to_id: string | null;
        user_id: string;
        user_name: string;
        content: AssetProps;
        created_at: string;
        updated_at: string | null;
      }>();
    return {
      id: comment.id,
      commentId: comment.comment_id,
      answerToId: comment.answer_to_id,
      user: {
        accountId: parseInt(comment.user_id),
        name: comment.user_name,
      },
      content: comment.content,
      createdAt: comment.created_at,
      updatedAt: comment.updated_at,
    };
  }

  async deleteAnswer(
    projectAccess: ProjectAccess,
    comment_id: string,
    reply_id: string,
    user: UserDTO,
  ): Promise<void> {
    await this._checkRightToReply(comment_id, projectAccess);
    await this._checkUserIsAuthorAndReturnReply(reply_id, user);

    await this.commentRepo.manager.transaction(async (tx) => {
      // меняю старый
      await tx
        .getRepository(CommentReplyEntity)
        .createQueryBuilder()
        .update(CommentReplyEntity)
        .where({
          id: reply_id,
          commentId: comment_id,
          projectId: projectAccess.projectId,
        })
        .set({ deletedAt: () => 'now()' })
        .execute();

      const existing_replies = await tx
        .getRepository(CommentReplyEntity)
        .findOne({
          commentId: comment_id,
          projectId: projectAccess.projectId,
          deletedAt: IsNull(),
          changedToId: IsNull(),
        });

      if (!existing_replies) {
        await tx
          .getRepository(CommentEntity)
          .createQueryBuilder()
          .update(CommentEntity)
          .where({ id: comment_id, projectId: projectAccess.projectId })
          .set({ deletedAt: () => 'now()' })
          .execute();
      }
    });
  }

  async getComments(
    projectAccess: ProjectAccess,
    comment_id: string,
    offset?: number,
    count?: number,
  ): Promise<GetCommentsResultDTO> {
    await this._checkRightToReply(comment_id, projectAccess);

    // сначала беру количество всех записей, а потом с offset, count фильтрую
    const dbquery = this.commentReplyRepo
      .createQueryBuilder('cr')
      .innerJoin(UserEntity, 'u', 'cr.user_id = u.id')
      .leftJoin(
        ProjectUserEntity,
        'pu',
        'pu.user_id = u.id AND pu.project_id = cr.project_id',
      )
      .select([
        'cr.id as id',
        'cr.comment_id as comment_id',
        'cr.answer_to_id as answer_to_id',
        'cr.user_id as user_id',
        'COALESCE(pu.user_name, u.name) as user_name',
        'cr.content as content',
        'cr.created_at as created_at',
        'cr.updated_at as updated_at',
      ])
      .where(
        'cr.comment_id = :comment_id AND cr.deleted_at IS NULL AND cr.changed_to_id IS NULL',
        {
          comment_id,
        },
      );

    dbquery.orderBy('cr.created_at', 'DESC');

    if (count) dbquery.limit(count);
    if (offset) dbquery.offset(offset);

    const list = await dbquery.getRawMany<{
      id: string;
      comment_id: string;
      answer_to_id: string | null;
      user_id: string;
      user_name: string;
      content: AssetProps;
      created_at: string;
      updated_at: string | null;
    }>();

    const replies: { [key: string]: CommentReplyDTO } = {};

    list.forEach((el) => {
      replies[el.id] = {
        id: el.id,
        commentId: el.comment_id,
        answerToId: el.answer_to_id,
        user: {
          accountId: parseInt(el.user_id),
          name: el.user_name,
        },
        content: el.content,
        createdAt: el.created_at,
        updatedAt: el.updated_at,
      };
    });
    return {
      ids: Object.keys(replies),
      objects: {
        replies,
      },
      more: false,
    };
  }

  async getCommentsByAssetIds(
    asset_ids: string[],
    context: AssetSelectorContext,
  ): Promise<AssetCommentDTO[]> {
    if (asset_ids.length === 0) return [];
    const comment_block_keys = await this.commentBlockKeyRepo
      .createQueryBuilder('cbk')
      .innerJoin(
        'comments',
        'com',
        'com.id = cbk.comment_id AND deleted_at IS NULL',
      )
      .innerJoin(
        'asset_block_keys',
        'abk',
        'cbk.block_key = abk.block_key AND abk.project_id = :project_id',
        {
          project_id: decodeBigNumberKey(context.projectId),
        },
      )
      .where('cbk.asset_id = ANY(:asset_ids)', { asset_ids })
      .select([
        'cbk.project_id as project_id',
        'cbk.asset_id as asset_id',
        'cbk.comment_id as comment_id',
        'cbk.anchor as anchor',
        'abk.block_name as name',
        'abk.block_title as title',
        'abk.block_type as type',
      ])
      .getRawMany();
    const result: AssetCommentDTO[] = [];
    for (const asset_id of asset_ids) {
      const asset_comments = comment_block_keys.filter(
        (cbk) => cbk.asset_id === asset_id,
      );
      const comment_map = new Map<string, CommentBlockDTO[]>();
      asset_comments.forEach((ac) => {
        const comments = comment_map.get(ac.comment_id);
        const block_key = stringifyAssetBlockRef(ac.name, ac.title, ac.type);
        if (comments) {
          comments.push({
            ref: block_key,
            anchor: ac.anchor,
          });
        } else {
          comment_map.set(ac.comment_id, [
            {
              ref: block_key,
              anchor: ac.anchor,
            },
          ]);
        }
      });
      for (const [comment_id, comment_blocks] of comment_map) {
        result.push({
          id: comment_id,
          assetId: asset_id,
          blocks: comment_blocks,
        });
      }
    }
    return result;
  }
}
