import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserDTO } from 'src/common/dto/user-dto';
import { ApiError } from 'src/common/error/api-error';
import { ApiErrorCodes } from 'src/common/error/api-error-codes';
import { ApiResultListWithTotal } from 'src/common/types/api-result';
import { ColumnEntity } from 'src/entities/column.entity';
import { ProjectUserEntity } from 'src/entities/project-user.entity';
import { ProjectEntity } from 'src/entities/project.entity';
import {
  ChangeProjectSettingsDTO,
  CreateProjectDTO,
  ProjectInfoDTO,
  ProjectQueryDTO,
} from 'src/project/dto/project-dto';
import {
  ProjectAccess,
  ProjectRole,
  checkProjectAccess,
} from 'src/utils/project-access';
import { Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import {
  decodeBigNumberKey,
  encodeBigNumberKey,
  generateBigNumberKey,
} from '../utils/big-number-key';
import { ProjectRoleEntity } from '../entities/project-role.entity';
import { QueryPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { ProjectService } from '../project/project.service';
import { AppLoadDTO, AppLoadProjectListItemDTO } from './dto/app-load-dto';
import { UserService } from '../user/user.service';
import { NotifierService } from '../notifier/notifier.service';
import {
  getDefaultLicense,
  getProjectLicense,
} from '../license/get-project-license';
import { ActualProjectLicenseEntity } from '../entities/actual-project-license.entity';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { ProjectLicense } from '../asset/logic/ProjectLicense';
import {
  UserExtAppToken,
  UserExtAppTokenProject,
  UserExtAppTokenUserRole,
} from './UserExtAppToken';

const shortLinkSpecialWordsSet = new Set(['page', 'app']);
const shortLinkAvailableSymbolsRegex = new RegExp(/^[a-zA-Z0-9\-]{3,}$/);

export type ApiTokenContent = {
  pid: string | null;
  userId: string;
  userRole: ProjectRole | null;
  projectLicense: ProjectLicense | null;
};

@Injectable()
export class AppService {
  constructor(
    @InjectRepository(ProjectEntity)
    private readonly projectRepo: Repository<ProjectEntity>,
    private readonly projectService: ProjectService,
    private readonly userService: UserService,
    private readonly notifierService: NotifierService,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
  ) {}

  async getProjectIdByShortLink(shortLink: string): Promise<string> {
    const res = await this.projectRepo
      .createQueryBuilder('p')
      .where('p.short_link = :shortLink', {
        shortLink: shortLink.toLowerCase(),
      })
      .select(['id'])
      .getRawOne();
    return res ? encodeBigNumberKey(res.id) : null;
  }

  async appLoad(
    account: UserDTO | null,
    project_access?: ProjectAccess,
  ): Promise<AppLoadDTO> {
    const user_id = account ? account.id : null;
    let unreadNotificationsCount = 0;
    if (user_id) {
      await this.userService.createUserIfNotExists(
        this.projectRepo.manager,
        account,
      );

      unreadNotificationsCount =
        await this.notifierService.getUnreadNotificationsCount(user_id);
    }

    const result: AppLoadDTO = {
      projects: [],
      project: null,
      role: null,
      unreadNotificationsCount,
    };

    if (user_id) {
      const projects = await this._getProjectListImpl(user_id, {});
      result.projects = projects.list;
    }
    if (project_access) {
      if (project_access.projectId) {
        result.project = await this.projectService.getProjectInfo(
          project_access.projectId,
        );
      }
      if (project_access.userRole) {
        result.role = {
          num: project_access.userRole.num,
          title: project_access.userRole.title,
          isAdmin: project_access.userRole.isAdmin,
        };
      } else {
        result.project.license = null;
      }
    }
    return result;
  }

  private async _getProjectListImpl(
    userId: number,
    params: ProjectQueryDTO,
  ): Promise<ApiResultListWithTotal<AppLoadProjectListItemDTO>> {
    const dbquery = this.projectRepo
      .createQueryBuilder('p')
      .innerJoin(
        `project_users`,
        'pu',
        'pu.project_id = p.id AND pu.user_id = :user_id AND pu.left_at IS NULL',
        {
          user_id: userId,
        },
      )
      .innerJoin(
        `project_roles`,
        'pr',
        'pr.project_id = p.id AND pr.num = pu.user_role_num',
      )
      .leftJoin(ActualProjectLicenseEntity, 'apl', 'apl.project_id = p.id')
      .select([
        'p.id as id',
        'p.title as title',
        'pr.title as role',
        'p.short_link as short_link',
        'apl.license_type_name as license_type_name',
        'apl.license_type_title as license_type_title',
        'apl.id as license_id',
        'apl.start_at as license_start_at',
        'apl.till as license_till',
        'apl.is_trial as license_is_trial',
      ]);

    if (params.where) {
      if (params.where.query) {
        dbquery.andWhere('p.title ILIKE :query', {
          query: `%${params.where.query}%`,
        });
      }
    }

    dbquery.orderBy('p.title', 'ASC');
    dbquery.addOrderBy('p.id', 'ASC');

    const total = await dbquery.getCount();

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

    const list = await dbquery.getRawMany<{
      id: string;
      title: string;
      role: string;
      short_link: string | null;
      license_type_name: string | null;
      license_type_title: string | null;
      license_id: number | null;
      license_start_at: Date | null;
      license_till: Date | null;
      license_is_trial: boolean | null;
    }>();

    const def = getDefaultLicense();

    return {
      list: list.map((el) => {
        return {
          id: encodeBigNumberKey(el.id),
          title: el.title,
          shortLink: el.short_link,
          role: (el as any).role,
          license: {
            id: el.license_id ? el.license_id : def.id,
            title: el.license_id ? el.license_type_title : def.title,
            name: el.license_id ? el.license_type_name : def.name,
            isTrial: el.license_id ? el.license_is_trial : def.isTrial,
            startAt:
              (el.license_id
                ? el.license_start_at
                : def.startAt
              )?.toISOString() ?? null,
            till:
              (el.license_id ? el.license_till : def.till)?.toISOString() ??
              null,
          },
        };
      }),
      total,
    };
  }

  async getProjectList(
    user: UserDTO,
    params: ProjectQueryDTO,
  ): Promise<ApiResultListWithTotal<AppLoadProjectListItemDTO>> {
    return this._getProjectListImpl(user.id, params);
  }

  async createProject(
    creator: UserDTO,
    params: CreateProjectDTO,
  ): Promise<ProjectInfoDTO> {
    const new_project_id = generateBigNumberKey();

    await this.projectRepo.manager.transaction(async (tx) => {
      //Создаю проект
      await tx.getRepository(ProjectEntity).insert({
        id: new_project_id,
        title: params.title,
        timezoneShift: params.timezoneShift,
      });

      // Добавляю стандартные колонки
      const base_columns = [
        '[[t:TaskColumnPlan]]',
        '[[t:TaskColumnInWork]]',
        '[[t:TaskColumnTest]]',
        '[[t:TaskColumnDone]]',
      ];

      for (let index = 0; index < base_columns.length; index++) {
        await tx.getRepository(ColumnEntity).insert({
          id: uuidv4(),
          projectId: new_project_id,
          title: base_columns[index],
          index: index + 1,
        });
      }

      await tx.query(
        `
        INSERT INTO public.assets (
          id,
          workspace_id,
          parent_ids,
          project_id,
          name,
          title,
          icon,
          is_abstract,
          creator_user_id,
          scope_id)
        VALUES (
          $1,
          null,
          '{00000000-0000-0000-0000-100000000000}',
          $2,
          'game_info_base',
          '[[t:GameInfo]]',
          'information-fill',
          false,
          null,
          4);
      `,
        [uuidv4(), decodeBigNumberKey(new_project_id)],
      );

      await tx.query(
        `
        INSERT INTO public.assets (
          id,
          workspace_id,
          parent_ids,
          project_id,
          name,
          title,
          icon,
          is_abstract,
          creator_user_id,
          scope_id)
        VALUES (
          $1,
          null,
          '{00000000-0000-0000-0000-100000000002}',
          $2,
          'project_props',
          '[[t:ProjectProps]]',
          'tools-fill',
          false,
          null,
          4);
      `,
        [uuidv4(), decodeBigNumberKey(new_project_id)],
      );

      await tx.query(
        `
        INSERT INTO public.asset_comps (
          id,
          project_id
        )
        SELECT id, project_id
        FROM assets
        WHERE project_id = $1
        ON CONFLICT DO NOTHING;
      `,
        [decodeBigNumberKey(new_project_id)],
      );

      await tx.query(
        `
        INSERT INTO public.asset_block_keys(
            project_id,
            block_key,
            block_name,
            block_title,
            block_type
        ) 
        SELECT 
            $1,
            block_key,
            block_name,
            block_title,
            block_type
        FROM asset_block_keys
        WHERE project_id = 1;
      `,
        [decodeBigNumberKey(new_project_id)],
      );

      // Roles
      await tx.getRepository(ProjectRoleEntity).insert({
        projectId: new_project_id,
        num: 1,
        isAdmin: true,
        title: '[[t:RoleLeader]]',
      });
      await tx.getRepository(ProjectRoleEntity).insert({
        projectId: new_project_id,
        num: 2,
        isDefault: true,
        title: '[[t:RoleMember]]',
      });

      //создаю пользователя в бд creators если его там еще нет
      await this.userService.createUserIfNotExists(tx, creator);

      //Привязываю лидера к проекту (по умолчанию - создатель проекта)
      await tx.getRepository(ProjectUserEntity).insert({
        projectId: new_project_id,
        user: creator.id,
        userRoleNum: 1,
      });
    });

    return await this.projectService.getProjectInfo(new_project_id);
  }

  async changeProject(
    projectAccess: ProjectAccess,
    params: ChangeProjectSettingsDTO,
  ): Promise<ProjectInfoDTO> {
    if (!projectAccess.userRole.isAdmin) {
      throw new ApiError(
        'User should be leader of project ' +
          projectAccess.projectId +
          ' to change project',
        ApiErrorCodes.ACCESS_DENIED,
      );
    }

    const changed_project: QueryPartialEntity<ProjectEntity> = {};

    if (params.title) {
      changed_project.title = params.title;
    }
    if (params.shortLink) {
      const res = await this.checkShortLink(params.shortLink, projectAccess);
      if (!res) {
        throw new ApiError(
          'Short link is not available to change project',
          ApiErrorCodes.ACCESS_DENIED,
        );
      }
      changed_project.shortLink = params.shortLink.toLowerCase();
    }
    if (params.lang) {
      changed_project.lang = params.lang;
    }
    if (typeof params.isUnsafeContent === 'boolean') {
      changed_project.isUnsafeContent = params.isUnsafeContent;
    }
    if (typeof params.isPublicGdd === 'boolean') {
      changed_project.isPublicGdd = params.isPublicGdd;
    }
    if (typeof params.isPublicTasks === 'boolean') {
      changed_project.isPublicTasks = params.isPublicTasks;
    }
    if (typeof params.isPublicAbout === 'boolean') {
      changed_project.isPublicAbout = params.isPublicAbout;
    }
    if (typeof params.isPublicPulse === 'boolean') {
      changed_project.isPublicPulse = params.isPublicPulse;
    }
    if (typeof params.timezoneShift === 'number') {
      changed_project.timezoneShift = params.timezoneShift;
    }
    changed_project.updatedAt = () => 'NOW()';

    // Делаю изменения в таблице projects
    await this.projectRepo
      .createQueryBuilder()
      .update(ProjectEntity)
      .set(changed_project)
      .where('id = :id', { id: decodeBigNumberKey(projectAccess.projectId) })
      .execute();

    return await this.projectService.getProjectInfo(projectAccess.projectId);
  }

  async deleteProject(projectAccess: ProjectAccess): Promise<void> {
    if (!projectAccess.userRole.isAdmin) {
      throw new ApiError(
        'User has no access to delete project ' + projectAccess.projectId,
        ApiErrorCodes.ACCESS_DENIED,
      );
    }

    await this.projectRepo
      .createQueryBuilder()
      .update(ProjectEntity)
      .where({ id: projectAccess.projectId })
      .set({ deletedAt: () => 'now()' })
      .execute();
  }

  async checkShortLink(
    link: string,
    projectAccess: ProjectAccess,
  ): Promise<boolean> {
    const short_link = link.toLowerCase();
    if (
      !shortLinkAvailableSymbolsRegex.test(short_link) ||
      shortLinkSpecialWordsSet.has(short_link)
    ) {
      return false;
    }
    if (!projectAccess.projectLicense.features.communityShortLink) {
      return false;
    }

    const project_ids: string[] = [];
    const need_del_link_project_ids: string[] = [];
    const all_projects = await this.projectRepo.find({
      shortLink: short_link,
    });

    for (const project of all_projects) {
      const project_license: ProjectLicense | null = await getProjectLicense(
        this.projectRepo.manager.connection,
        project.id,
      );
      if (project_license?.features.communityShortLink) {
        if (projectAccess.projectId === project.id) continue;
        project_ids.push(project.id);
      } else {
        need_del_link_project_ids.push(project.id);
      }
    }

    if (need_del_link_project_ids.length > 0) {
      await this.projectRepo
        .createQueryBuilder()
        .update(ProjectEntity)
        .set({
          shortLink: null,
        })
        .where('id = ANY(:project_ids)', {
          project_ids: need_del_link_project_ids.map((item) =>
            decodeBigNumberKey(item),
          ),
        })
        .execute();
    }

    return project_ids.length === 0;
  }

  // Содержимое токена
  //  {
  //   pid: string | null,
  //   userId: string,
  //   userRole: ProjectRole | null,
  //   projectLicense: ProjectLicense | null;
  //  }
  async getExtAppToken(
    projectIds: string[],
    account: UserDTO,
    license_features: string[],
  ): Promise<string> {
    const token_content = new UserExtAppToken();
    token_content.userId = encodeBigNumberKey(account.id.toString());
    token_content.userName = account.name;
    token_content.projects = [];
    for (const projectId of projectIds) {
      const project_access = await checkProjectAccess(
        this.projectRepo.manager.connection,
        account.id,
        projectId,
      );
      if (project_access.userRole && project_access.projectLicense) {
        const role: UserExtAppTokenUserRole = {
          num: project_access.userRole.num,
          title: project_access.userRole.title,
        };
        if (project_access.userRole.isAdmin) {
          role.admin = true;
        }
        const project: UserExtAppTokenProject = {
          id: projectId,
          userRole: role,
        };
        if (license_features.length > 0) {
          project.features = {};
          for (const f of license_features) {
            project.features[f] = project_access.projectLicense?.features[f];
          }
        }
        token_content.projects.push(project);
      }
    }

    return this.jwtService.sign(
      { ...token_content },
      {
        expiresIn: '120s',
        subject: account.id.toString(),
        algorithm: 'RS256',
        privateKey: this.configService.get('gamemanager').privateKey,
      },
    );
  }

  /*
  const dbquery = this.projectRepo
      .createQueryBuilder('p')
      .innerJoin(
        'project_licenses',
        'pl',
        'pl.project_id = pr.project_id AND pu.user_role_num = pr.num',
      )
      .where('p.short_link = :short_link', {
        short_link: link,
      });

    const total = await dbquery.getCount();
    return total > 0;
  */

  /*
  async getTemplateList(
    params: TemplateQueryDTO,
  ): Promise<ApiResultListWithTotal<ProjectEntity>> {
    const dbquery = this.projectRepo
      .createQueryBuilder('p')
      .where('p.template_id IS NOT NULL');

    if (params.where) {
    }

    dbquery.orderBy('p.title', 'ASC');
    dbquery.addOrderBy('p.id', 'ASC');

    const total = await dbquery.getCount();

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

    const list = await dbquery.getMany();

    return {
      list: list,
      total,
    };
  }

  async createTemplate(
    creator: UserDTO,
    params: CreateProjectDTO,
  ): Promise<any> {
    const new_project_id = generateBigNumberKey();
    const new_template_id = uuidv4();
    //Создаю проект
    await this.projectRepo.insert({
      id: new_project_id,
      title: params.title,
      templateId: new_template_id,
    });

    // Добавляю стандартные колонки
    const base_columns =
      creator.language === 'ru'
        ? ['План', 'В работе', 'Готово']
        : ['Plan', 'In work', 'Done']; //

    for (const col of base_columns) {
      await this.columnRepo.insert({
        id: uuidv4(),
        projectId: new_project_id,
        title: col,
      });
    }

    //создаю пользователя в бд creators если его там еще нет
    this._createUserIfNotExists(creator);

    //Привязываю лидера к проекту (по умолчанию - создатель проекта)
    await this.projectUserRepo.insert({
      projectId: new_project_id,
      user: creator.id,
      role: 'leader',
    });

    return await this.projectRepo.findOne({
      id: new_project_id,
      templateId: new_template_id,
    });
  }

  async deleteTemplate(account: UserDTO, template_id: string): Promise<void> {
    await this.projectRepo
      .createQueryBuilder()
      .update(ProjectEntity)
      .where({ templateId: template_id })
      .set({ deletedAt: () => 'now()' })
      .execute();
  }*/
}
