import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { ColumnEntity } from '../entities/column.entity';
import { ApiResultListWithTotal } from 'src/common/types/api-result';
import { TaskDTO } from './dto/task-dto';
import { ColumnDTO, ColumnQueryDTO } from './dto/column-dto';
import { ApiError } from 'src/common/error/api-error';
import { ApiErrorCodes } from 'src/common/error/api-error-codes';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { decodeBigNumberKey } from '../utils/big-number-key';
import {
  AssetSelectorContext,
  AssetSelectorService,
} from '../asset/asset-selector.service';
import {
  AssetPropWhere,
  AssetPropWhereOpKind,
} from '../asset/logic/PropsWhere';
import {
  AssetPropValueAccount,
  castAssetPropValueToFloat,
  castAssetPropValueToInt,
  castAssetPropValueToString,
  isCheckedAssetPropValue,
} from '../asset/logic/Props';
import { IVANOV_PROJECT_ID, TASK_ASSET_ID } from '../constants';
import { assignRightsFilterInSelection } from '../asset/logic/assignRightsFilterInSelection';
import { MIN_ASSET_RIGHTS_TO_READ } from '../asset/logic/Rights';
import { AccessTag, ProjectAccess } from '../utils/project-access';
import { AssetPropAccountDTO } from '../asset/dto/block-dto';
import { AssetChanger } from '../asset/logic/AssetChanger';
import { ComputingService } from '../asset/computing.service';
import { AssetPropsSelectionBase } from '../asset/logic/PropsSelection';

@Injectable()
export class TaskService {
  constructor(
    @InjectRepository(ColumnEntity)
    private readonly columnRepo: Repository<ColumnEntity>,
    private readonly assetSelectorService: AssetSelectorService,
    private readonly computingService: ComputingService,
  ) {}

  // Методы для работы с колонками

  private async _checkCanChangeTaskBoard(projectAccess: ProjectAccess) {
    if (projectAccess.accessTags.has(AccessTag.ROOT)) {
      return;
    }
    if (!projectAccess.userRole) {
      throw new ApiError(
        'No rights to change task board',
        ApiErrorCodes.ACCESS_DENIED,
      );
    }
  }
  private async _canGetTaskBoard(
    projectAccess: ProjectAccess,
  ): Promise<boolean> {
    if (projectAccess.accessTags.has(AccessTag.ROOT)) {
      return true;
    }
    if (!projectAccess.userRole) {
      if (!projectAccess.projectParams.isPublicTasks) {
        return false;
      }
    }
    return true;
  }

  async getColumns(
    params: ColumnQueryDTO,
    projectAccess: ProjectAccess,
  ): Promise<ApiResultListWithTotal<ColumnEntity>> {
    const can_get = await this._canGetTaskBoard(projectAccess);
    if (!can_get) {
      return {
        list: [],
        total: 0,
      };
    }
    const dbquery = this.columnRepo
      .createQueryBuilder('c')
      .where('c.deleted_at IS NULL AND c.project_id = :project_id', {
        project_id: decodeBigNumberKey(projectAccess.projectId),
      }); //Фильтрую удаленные записи и передаю только те, которые находятся в доступных для пользователя проектах

    if (params.where) {
    }

    dbquery.orderBy('c.index', 'ASC');
    dbquery.addOrderBy('c.title', 'ASC');
    dbquery.addOrderBy('c.created_at', 'ASC');
    dbquery.addOrderBy('c.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 createColumn(
    title: string,
    projectAccess: ProjectAccess,
  ): Promise<ColumnEntity> {
    await this._checkCanChangeTaskBoard(projectAccess);
    const new_column_id = uuidv4();

    const new_column: QueryDeepPartialEntity<ColumnEntity> = {
      id: new_column_id,
      projectId: projectAccess.projectId,
      title,
    };

    await this.columnRepo.insert(new_column);
    return await this.columnRepo.findOneOrFail({ id: new_column_id });
  }

  async reorderColumns(column_ids: string[], projectAccess: ProjectAccess) {
    await this._checkCanChangeTaskBoard(projectAccess);
    await this.columnRepo.manager.transaction(async (tr) => {
      let i = 1;
      for (const column_id of column_ids) {
        tr.createQueryBuilder()
          .update(ColumnEntity)
          .set({ index: i++, updatedAt: () => 'now()' })
          .where(
            'id = :id AND project_id = :project_id AND deleted_at IS NULL',
            {
              id: column_id,
              project_id: decodeBigNumberKey(projectAccess.projectId),
            },
          )
          .execute();
      }
    });
  }

  async editColumn(
    column_id: string,
    params: ColumnDTO,
    projectAccess: ProjectAccess,
  ): Promise<ColumnEntity> {
    await this._checkCanChangeTaskBoard(projectAccess);
    await this._hasProjectColumn(column_id, projectAccess.projectId);

    await this.columnRepo
      .createQueryBuilder()
      .update(ColumnEntity)
      .set({ title: params.title, updatedAt: () => 'now()' })
      .where('id = :id', { id: column_id })
      .execute();

    return this._getProjectColumn(column_id, projectAccess.projectId);
  }

  async deleteColumn(
    column_id: string,
    projectAccess: ProjectAccess,
  ): Promise<void> {
    await this._checkCanChangeTaskBoard(projectAccess);
    await this._hasProjectColumn(column_id, projectAccess.projectId);

    // При удалении колонки все задачи этой колонки помещаются в архив
    const tasks_of_column = await this.assetSelectorService.getView(
      {
        where: {
          typeids: TASK_ASSET_ID,
          'info||props|\\taskcolumn': column_id,
          'info||props|\\archivedat': null,
        },
        select: ['id'],
      },
      projectAccess,
    );

    const changers: AssetChanger[] = [];
    const now = new Date().toISOString();
    for (const task of tasks_of_column) {
      const changer = await AssetChanger.ChangeAssets(
        {
          ...projectAccess,
          tx: this.columnRepo.manager,
        },
        {
          id: task.id as string,
        },
        {
          props: {
            'info||props': {
              '\\archivedat': now,
            },
          },
        },
      );
      changers.push(changer);
    }

    //Удаляю колонку
    await this.columnRepo.manager.transaction(async (tx) => {
      for (const changer of changers) {
        await changer.commit(tx);
      }
      tx.createQueryBuilder()
        .update(ColumnEntity)
        .set({ deletedAt: () => 'now()' })
        .where('id = :column_id', { column_id: column_id })
        .execute();
    });

    await this.computingService.runComputeForAssets(
      projectAccess.projectId,
      tasks_of_column.map((r) => r.id as string),
    );
  }

  // Методы для работы с задачами
  async getTasks(
    selection: AssetPropsSelectionBase,
    context: AssetSelectorContext,
  ): Promise<ApiResultListWithTotal<TaskDTO>> {
    const where: AssetPropWhere = {
      typeids: TASK_ASSET_ID,
    };

    if (!context.userRole) {
      where['info||props|\\archivedat'] = null;
    }

    if (
      context.projectId === IVANOV_PROJECT_ID &&
      !(context.userRole && context.userRole.isAdmin)
    ) {
      where['useraccess'] = {
        op: AssetPropWhereOpKind.OR,
        v: [
          {
            ['creatoruserid']: context.userId,
          },
          {
            ['info||props|\\assignedto']: {
              accountId: context.userId,
            },
          },
          ...[...Array(10)].map((x, i) => {
            return {
              [`info||props|\\watchers\\${i}`]: {
                accountId: context.userId,
              },
            };
          }),
        ],
      };
    }

    const res = await this.assetSelectorService.getViewPaginated(
      assignRightsFilterInSelection(
        {
          select: [
            'id',
            'title',
            'basic||props|\\num',
            'info||props|\\assignedto',
            'info||props|\\iscompleted',
            'info||props|\\result',
            'info||props|\\archivedat',
            'info||props|\\taskcolumn',
            'index',
            'info||props|\\color',
            'info||props|\\estimatedtime',
            'info||props|\\actualtime',
            'info||props|\\scheduledat',
            'isattracting',
            'createdat',
          ],
          count: selection.count,
          offset: selection.offset,
          where: { ...selection.where, ...where },
          order: selection.order
            ? selection.order
            : ['index', { prop: 'createdat', desc: true }],
        },
        MIN_ASSET_RIGHTS_TO_READ,
      ),
      {
        ...context,
        accessTags: new Set([...context.accessTags, AccessTag.TASKS]),
      },
    );
    return {
      list: res.list.map((av) => {
        let assignedTo: AssetPropAccountDTO | null = null;
        if (av['info||props|\\assignedto']) {
          assignedTo = av['info||props|\\assignedto'] as AssetPropValueAccount;
        }
        return {
          id: av.id as string,
          title: av.title as string,
          num: av['basic||props|\\num']
            ? castAssetPropValueToInt(av['basic||props|\\num'])
            : null,
          columnId: av['info||props|\\taskcolumn']
            ? castAssetPropValueToString(av['info||props|\\taskcolumn'])
            : null,
          result: av['info||props|\\result'],
          assignedTo,
          archivedAt: av['info||props|\\archivedat']
            ? castAssetPropValueToString(av['info||props|\\archivedat'])
            : null,
          isCompleted: isCheckedAssetPropValue(av['info||props|\\iscompleted']),
          isAttracting: isCheckedAssetPropValue(av['isattracting']),
          index: castAssetPropValueToFloat(av['index']),
          color: av['info||props|\\color']
            ? castAssetPropValueToString(av['info||props|\\color'])
            : null,
          scheduledAt: av['info||props|\\scheduledat']
            ? castAssetPropValueToString(av['info||props|\\scheduledat'])
            : null,
          actualTime: av['info||props|\\actualtime']
            ? castAssetPropValueToFloat(av['info||props|\\actualtime'])
            : null,
          estimatedTime: av['info||props|\\estimatedtime']
            ? castAssetPropValueToFloat(av['info||props|\\estimatedtime'])
            : null,
          createdAt: av.createdat as string,
        };
      }),
      total: res.total,
    };
  }

  private async _hasProjectColumn(
    column_id: string,
    project_id: string,
  ): Promise<ColumnEntity> {
    const column = await this._getProjectColumn(column_id, project_id);
    if (!column) {
      throw new ApiError(
        `Column with id = ${column_id} doesn't exist in project ${project_id}`,
        ApiErrorCodes.ENTITY_NOT_FOUND,
      );
    }
    return column;
  }

  private async _getProjectColumn(
    column_id: string,
    project_id: string,
  ): Promise<ColumnEntity> {
    return await this.columnRepo.findOne({
      id: column_id,
      projectId: project_id,
      deletedAt: null,
    });
  }
}
