import { Injectable } from '@nestjs/common';
import { User, newUser } from './entity/user.entity';
import { DataSource, IsNull, Not, UpdateResult } from 'typeorm';
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
import {
  getDirection,
  getTaskListSortableAttribute,
} from '../../common/types/sort/util';
import {
  UserNotFoundError,
  EmailAlreadyVerifiedError,
  AuthorIdAlreadyExistsError,
  InvalidRoleChangeError,
  EncryptionPasswordNeedError,
} from './errors/types';
import {
  LICENSE_ALLOCATED_STATUS,
  LICENSE_TYPE,
  TRIAL_LICENSE_ISSUE_NUM,
  USER_ROLES,
} from '../../constants';
import { License } from '../licenses/entity/license.entity';
import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types';

@Injectable()
export class UsersRepositoryService {
  constructor(private dataSource: DataSource) {}

  /**
   * 一般ユーザーを作成する
   * @param user
   * @returns User
   */
  async createNormalUser(user: newUser): Promise<User> {
    const {
      account_id: accountId,
      external_id: externalUserId,
      role,
      auto_renew,
      license_alert,
      notification,
      author_id,
      accepted_terms_version,
      encryption,
      encryption_password: encryptionPassword,
      prompt,
    } = user;
    const userEntity = new User();

    userEntity.role = role;
    userEntity.account_id = accountId;
    userEntity.external_id = externalUserId;
    userEntity.auto_renew = auto_renew;
    userEntity.license_alert = license_alert;
    userEntity.notification = notification;
    userEntity.author_id = author_id;
    userEntity.accepted_terms_version = accepted_terms_version;
    userEntity.encryption = encryption;
    userEntity.encryption_password = encryptionPassword;
    userEntity.prompt = prompt;

    const createdEntity = await this.dataSource.transaction(
      async (entityManager) => {
        const repo = entityManager.getRepository(User);
        const newUser = repo.create(userEntity);
        const persisted = await repo.save(newUser);

        // ユーザーのタスクソート条件を作成
        const sortCriteria = new SortCriteria();
        {
          sortCriteria.parameter = getTaskListSortableAttribute('JOB_NUMBER');
          sortCriteria.direction = getDirection('ASC');
          sortCriteria.user_id = persisted.id;
        }
        const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
        const newSortCriteria = sortCriteriaRepo.create(sortCriteria);
        await sortCriteriaRepo.save(newSortCriteria);

        return persisted;
      },
    );
    return createdEntity;
  }

  async findVerifiedUser(sub: string): Promise<User | undefined> {
    const user = await this.dataSource.getRepository(User).findOne({
      where: {
        external_id: sub,
        email_verified: true,
      },
    });

    if (!user) {
      return undefined;
    }
    return user;
  }

  async findUserByExternalId(sub: string): Promise<User> {
    const user = await this.dataSource.getRepository(User).findOne({
      where: {
        external_id: sub,
      },
      relations: {
        account: true,
      },
    });

    if (!user) {
      throw new UserNotFoundError();
    }
    return user;
  }

  async findUserById(id: number): Promise<User | undefined> {
    const user = await this.dataSource.getRepository(User).findOne({
      where: {
        id: id,
      },
    });

    if (!user) {
      return undefined;
    }
    return user;
  }

  /**
   * AuthorIdが既に存在するか確認する
   * @param user
   * @returns 存在する：true 存在しない：false
   */
  async existsAuthorId(
    accountId: number,
    authorId: string,
  ): Promise<boolean | undefined> {
    const user = await this.dataSource.getRepository(User).findOne({
      where: [
        {
          account_id: accountId,
          author_id: authorId,
        },
      ],
    });

    if (user) {
      return true;
    }
    return false;
  }

  /**
   * 特定の情報でユーザーを更新する
   * @param user
   * @returns update
   */
  async update(
    accountId: number,
    id: number,
    role: string,
    authorId: string | undefined,
    autoRenew: boolean,
    licenseAlart: boolean,
    notification: boolean,
    encryption: boolean | undefined,
    encryptionPassword: string | undefined,
    prompt: boolean | undefined,
  ): Promise<UpdateResult> {
    return await this.dataSource.transaction(async (entityManager) => {
      const repo = entityManager.getRepository(User);

      // 変更対象のユーザーを取得
      const targetUser = await repo.findOne({
        where: { id: id, account_id: accountId },
      });

      // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
      if (!targetUser) {
        throw new UserNotFoundError();
      }

      // ユーザーのロールがNoneの場合以外はロールを変更できない
      if (targetUser.role !== USER_ROLES.NONE && targetUser.role !== role) {
        throw new InvalidRoleChangeError('Not None user cannot change role.');
      }

      if (role === USER_ROLES.AUTHOR) {
        // ユーザーのロールがAuthorの場合はAuthorIDの重複チェックを行う
        const authorIdDuplicatedUser = await repo.findOne({
          where: { account_id: accountId, id: Not(id), author_id: authorId },
        });

        // 重複したAuthorIDがあった場合はエラー
        if (authorIdDuplicatedUser) {
          throw new AuthorIdAlreadyExistsError('authorId already exists.');
        }

        // 暗号化を有効にする場合はパスワードを設定する
        if (encryption) {
          // 暗号化パスワードが設定されている場合は更新する（undefinedの場合は変更なしとして元のパスワードを維持）
          if (encryptionPassword) {
            targetUser.encryption_password = encryptionPassword;
          } else if (!targetUser.encryption_password) {
            // DBにパスワードが設定されていない場合にはパスワードを設定しないとエラー
            throw new EncryptionPasswordNeedError(
              'encryption_password need to set value.',
            );
          }
        } else {
          targetUser.encryption_password = null;
        }

        // Author用項目を更新
        targetUser.author_id = authorId;
        targetUser.encryption = encryption;
        targetUser.prompt = prompt;
      } else {
        // ユーザーのロールがAuthor以外の場合はAuthor用項目はundefinedにする
        targetUser.author_id = null;
        targetUser.encryption = false;
        targetUser.encryption_password = null;
        targetUser.prompt = false;
      }

      // 共通項目を更新
      targetUser.role = role;
      targetUser.auto_renew = autoRenew;
      targetUser.license_alert = licenseAlart;
      targetUser.notification = notification;

      const result = await repo.update({ id: id }, targetUser);

      // 想定外の更新が行われた場合はロールバックを行った上でエラー送出
      if (result.affected !== 1) {
        throw new Error(`invalid update. result.affected=${result.affected}`);
      }
      return result;
    });
  }

  /**
   * 管理ユーザーがメール認証済みなら認証情報を更新する
   * @param user
   * @returns update
   */
  async updateUserVerified(id: number): Promise<UpdateResult> {
    return await this.dataSource.transaction(async (entityManager) => {
      const repo = entityManager.getRepository(User);
      const targetUser = await repo.findOne({
        where: {
          id: id,
        },
      });

      // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
      if (!targetUser) {
        throw new UserNotFoundError();
      }

      if (targetUser.email_verified) {
        throw new EmailAlreadyVerifiedError();
      }

      targetUser.email_verified = true;

      return await repo.update({ id: targetUser.id }, targetUser);
    });
  }

  /**
   * Emailを認証済みにして、トライアルライセンスを作成する
   * @param id
   * @returns user verified and create trial license
   */
  async updateUserVerifiedAndCreateTrialLicense(id: number): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const userRepo = entityManager.getRepository(User);
      const targetUser = await userRepo.findOne({
        relations: {
          account: true,
        },
        where: {
          id: id,
        },
      });

      // 運用上ユーザがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
      if (!targetUser) {
        throw new UserNotFoundError();
      }

      if (targetUser.email_verified) {
        throw new EmailAlreadyVerifiedError();
      }

      targetUser.email_verified = true;

      await userRepo.update({ id: targetUser.id }, targetUser);

      // トライアルライセンス100件を作成する
      const licenseRepo = entityManager.getRepository(License);
      const licenses: License[] = [];

      // トライアルライセンスの有効期限は今日を起算日として30日後の日付が変わるまで
      const expiryDate = new NewTrialLicenseExpirationDate();

      for (let i = 0; i < TRIAL_LICENSE_ISSUE_NUM; i++) {
        const license = new License();
        license.expiry_date = expiryDate;
        license.account_id = targetUser.account.id;
        license.type = LICENSE_TYPE.TRIAL;
        license.status = LICENSE_ALLOCATED_STATUS.UNALLOCATED;
        licenses.push(license);
      }

      await licenseRepo
        .createQueryBuilder()
        .insert()
        .into(License)
        .values(
          licenses.map((value) => ({
            expiry_date: value.expiry_date,
            account_id: value.account_id,
            type: value.type,
            status: value.status,
          })),
        )
        .execute();
    });
  }

  /**
   * 同じアカウントIDを持つユーザーを探す
   * @param externalId
   * @returns User[]
   */
  async findSameAccountUsers(external_id: string): Promise<User[]> {
    return await this.dataSource.transaction(async (entityManager) => {
      const repo = entityManager.getRepository(User);

      const accountId = (await repo.findOne({ where: { external_id } }))
        .account_id;

      const dbUsers = await this.dataSource.getRepository(User).find({
        relations: {
          userGroupMembers: {
            userGroup: true,
          },
          license: true,
        },
        where: { account_id: accountId },
      });

      return dbUsers;
    });
  }

  /**
   * アカウント内のタイピストユーザーを取得する
   * @param sub
   * @returns typist users
   */
  async findTypistUsers(sub: string): Promise<User[]> {
    return await this.dataSource.transaction(async (entityManager) => {
      const repo = entityManager.getRepository(User);

      const user = await repo.findOne({
        where: {
          external_id: sub,
        },
      });

      const typists = await repo.find({
        where: {
          account_id: user.account_id,
          role: USER_ROLES.TYPIST,
          deleted_at: IsNull(),
        },
      });

      return typists;
    });
  }

  /**
   * UserID指定のユーザーとソート条件を同時に削除する
   * @param userId
   * @returns delete
   */
  async deleteNormalUser(userId: number): Promise<void> {
    await this.dataSource.transaction(async (entityManager) => {
      const usersRepo = entityManager.getRepository(User);
      const sortCriteriaRepo = entityManager.getRepository(SortCriteria);
      // ソート条件を削除
      await sortCriteriaRepo.delete({
        user_id: userId,
      });
      // プライマリ管理者を削除
      await usersRepo.delete({ id: userId });
    });
  }
}
