import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AssetEntity } from '../entities/asset.entity';
import {
  ApiResultListWithMore,
  ApiResultListWithTotal,
} from 'src/common/types/api-result';
import { AssetFullDTO, AssetShortDTO, AssetViewDTO } from './dto/asset-dto';
import { AssetBlockDTO } from './dto/block-dto';
import { ApiError } from 'src/common/error/api-error';
import { ApiErrorCodes } from 'src/common/error/api-error-codes';
import { UserEntity } from 'src/entities/user.entity';
import { decodeBigNumberKey } from '../utils/big-number-key';
import { ComputingService } from './computing.service';
import { AssetsFullResultDTO } from './dto/assets-full-result-dto';
import {
  AssetSelectorContext,
  AssetSelectorService,
  AssetShort,
} from './asset-selector.service';
import { AssetPropWhere } from './logic/PropsWhere';
import {
  AssetPropsSelection,
  AssetPropsSelectionBase,
} from './logic/PropsSelection';
import { AssetsGraphResultDTO } from './dto/assets-graph-result-dto';
import { AssetRefEntity } from '../entities/asset-ref.entity';
import { AssetCreateDTO } from './dto/asset-change-dto';
import { AssetScope, TASK_ASSET_ID } from '../constants';
import { RefService } from './ref.service';
import {
  MIN_ASSET_RIGHTS_TO_HISTORY,
  MIN_ASSET_RIGHTS_TO_INDEX,
  MIN_ASSET_RIGHTS_TO_READ,
} from './logic/Rights';
import { assignRightsFilterInSelection } from './logic/assignRightsFilterInSelection';
import { getAllParentIdsOfAssetIdsTx } from './asset-helpers';
import { AssetChanger } from './logic/AssetChanger';
import { AssetBlock, getAssetAllBlocksTx } from './block-helpers';
import { ColumnEntity } from '../entities/column.entity';
import { AccessTag, ProjectAccess } from '../utils/project-access';
import { AssetReorderDTO } from './dto/asset-reorder-dto';
import {
  ActionType,
  ProjectActionEntity,
} from '../entities/project-action.entity';
import { v4 as uuidv4 } from 'uuid';
import { IvanovAssetService } from './customizations/ivanov-asset.service';
import { CommentService } from './comment.service';
import { AssetHistoryDTO, AssetHistoryQueryDTO } from './dto/history-dto';
import { AssetChangeAssetEntity } from '../entities/asset-change-asset.entity';
import { AssetChangeEntity } from '../entities/asset-change.entity';
import { ProjectUserEntity } from '../entities/project-user.entity';
import {IzhAutosnabService} from "./customizations/izh-autosnab-service";

@Injectable()
export class AssetService {
  constructor(
    private readonly assetSelectorService: AssetSelectorService,
    @InjectRepository(AssetEntity)
    private readonly assetRepo: Repository<AssetEntity>,
    @InjectRepository(UserEntity)
    private readonly userRepo: Repository<UserEntity>,
    @InjectRepository(AssetRefEntity)
    private readonly refRepo: Repository<AssetRefEntity>,
    @InjectRepository(ProjectActionEntity)
    private readonly projectActionRepo: Repository<ProjectActionEntity>,
    @InjectRepository(AssetChangeAssetEntity)
    private readonly assetChangeAssetEntityRepo: Repository<AssetChangeAssetEntity>,
    private readonly refService: RefService,
    private readonly commentService: CommentService,
    private readonly computingService: ComputingService,
    private readonly ivanovAssetService: IvanovAssetService,
    private readonly IzhAutosnabAssetService: IzhAutosnabService,
  ) {}

  private async _checkCanCreateAssetOrThrow(
    context: AssetSelectorContext,
    parent_ids: string[],
    workspace_id: string | null,
    scope: string,
  ) {
    if (context.accessTags.has(AccessTag.ROOT)) {
      return;
    }
    if (!context.userRole) {
      throw new ApiError(
        'No rights to create asset',
        ApiErrorCodes.ACCESS_DENIED,
      );
    }
    if (context.projectId) {
      const assets_count = await this.assetRepo.count({
        projectId: context.projectId,
        deletedAt: null,
      });
      const max_assets = context.projectLicense.features?.maxAssets;
      if (max_assets && assets_count > max_assets) {
        throw new ApiError(
          'Assets limit exceeded',
          ApiErrorCodes.LIMIT_EXCEEDED,
          {
            max: max_assets,
          },
        );
      }
    }
  }

  async createAsset(
    change: AssetCreateDTO,
    context: AssetSelectorContext,
    set_asset_id?: string,
  ): Promise<string> {
    await this._checkCanCreateAssetOrThrow(
      context,
      change.main.parentIds ?? [],
      change.main.workspaceId ?? null,
      change.main.scope ?? AssetScope.DEFAULT,
    );

    // For creating task
    if (change.main.parentIds && change.main.parentIds[0] === TASK_ASSET_ID) {
      change = {
        ...change,
        blocks: change.blocks ? { ...change.blocks } : {},
        props: change.props ? { ...change.props } : {},
      };
      change.blocks['basic||props'] = {
        name: 'basic',
        title: '[[t:BasicInfo]]',
      };
      const next_task_num = await this.getNextTaskNum(context);
      change.props['basic||props'] = {
        '\\num': next_task_num,
      };
      if (!change.props['info||props']) change.props['info||props'] = {};
      if (!change.props['info||props']['\\taskcolumn']) {
        change.props['info||props']['\\taskcolumn'] =
          await this.getDefaulTaskColumnId(context);
      }
    }

    const asset_changer = await AssetChanger.CreateAsset(
      {
        ...context,
        tx: this.assetRepo.manager,
      },
      {
        ...change,
        main: {
          ...(change.main ? change.main : {}),
          delete: undefined,
          restore: undefined,
        },
      },
      set_asset_id,
    );
    const asset_id = asset_changer.getAffectingAssetIds()[0];

    await asset_changer.commit();

    await this.computingService.runComputeForAssets(context.projectId, [
      asset_id,
    ]);

    await this.ivanovAssetService.assetPostUpdate(
      this,
      change,
      [asset_id],
      context,
    );

    await this.IzhAutosnabAssetService.assetPostUpdate(
      this,
      change,
      [asset_id],
      context,
    );

    return asset_id;
  }

  async changeAssets(
    where: AssetPropWhere,
    change: AssetCreateDTO,
    context: AssetSelectorContext,
  ): Promise<string[]> {
    const asset_changer = await AssetChanger.ChangeAssets(
      {
        ...context,
        tx: this.assetRepo.manager,
      },
      {
        ...where,
        isSystem: false,
      },
      {
        ...change,
        main: {
          ...(change.main ? change.main : {}),
          delete: undefined,
          restore: undefined,
        },
      },
    );

    const changed_asset_ids = asset_changer.getAffectingAssetIds();
    await asset_changer.commit();

    await this.computingService.runComputeForAssets(
      context.projectId,
      changed_asset_ids,
    );

    await this.ivanovAssetService.assetPostUpdate(
      this,
      change,
      changed_asset_ids,
      context,
    );

    await this.IzhAutosnabAssetService.assetPostUpdate(
      this,
      change,
      changed_asset_ids,
      context,
    );
    await this.checkNeedNotification(change, context, changed_asset_ids);

    return changed_asset_ids;
  }

  async deleteAssets(
    where: AssetPropWhere,
    context: AssetSelectorContext,
  ): Promise<string[]> {
    const asset_changer = await AssetChanger.ChangeAssets(
      {
        ...context,
        tx: this.assetRepo.manager,
      },
      {
        ...where,
        issystem: false,
      },
      {
        main: {
          delete: true,
        },
      },
    );

    const changed_asset_ids = asset_changer.getAffectingAssetIds();
    await asset_changer.commit();

    return changed_asset_ids;
  }

  async restoreAssets(
    where: AssetPropWhere,
    context: AssetSelectorContext,
  ): Promise<string[]> {
    const asset_changer = await AssetChanger.ChangeAssets(
      {
        ...context,
        tx: this.assetRepo.manager,
      },
      {
        ...where,
        issystem: false,
        withdeleted: true,
      },
      {
        main: {
          restore: true,
        },
      },
    );

    const changed_asset_ids = asset_changer.getAffectingAssetIds();
    await asset_changer.commit();

    return changed_asset_ids;
  }

  _convertAssetShortToDTO(asset: AssetShort): AssetShortDTO {
    return {
      id: asset.id,
      typeIds: asset.typeIds,
      title: asset.title,
      name: asset.name,
      icon: asset.icon,
      isAbstract: asset.isAbstract,
      readinessRate: asset.readinessRate,
      createdAt: asset.createdAt,
      updatedAt: asset.updatedAt,
      deletedAt: asset.deletedAt,
      rights: asset.rights,
      scope: asset.scope,
      workspaceId: asset.workspaceId,
      index: asset.index,
    };
  }

  async getAssetsShort(
    selection: AssetPropsSelectionBase,
    context: AssetSelectorContext,
  ): Promise<ApiResultListWithTotal<AssetShortDTO>> {
    const result = await this.assetSelectorService.getShortsPaginated(
      assignRightsFilterInSelection(selection, MIN_ASSET_RIGHTS_TO_READ),
      context,
    );

    return {
      list: result.list.map((ash) => this._convertAssetShortToDTO(ash)),
      total: result.total,
    };
  }

  async getAssetsView(
    selection: AssetPropsSelection,
    context: AssetSelectorContext,
  ): Promise<ApiResultListWithTotal<AssetViewDTO>> {
    return await this.assetSelectorService.getViewPaginated(
      assignRightsFilterInSelection(selection, MIN_ASSET_RIGHTS_TO_READ),
      context,
    );
  }

  private _convertAssetBlockToDTO(block: AssetBlock): AssetBlockDTO {
    return {
      name: block.name,
      title: block.title ? block.title : block.blockKeyTitle,
      ownTitle: block.title,
      type: block.type,
      props: block.props,
      // computedProps: block.computedProps,
      index: block.index,
      createdAt: block.createdAt.toISOString(),
      updatedAt: block.updatedAt.toISOString(),
      rights: block.rights,
      filledRate: block.filledRate,
    };
  }

  async getAssetsFull(
    selection: AssetPropsSelectionBase,
    context: AssetSelectorContext,
    userView: boolean,
  ): Promise<AssetsFullResultDTO> {
    const matched_asset_ids = await this.assetSelectorService.getIdsPaginated(
      assignRightsFilterInSelection(selection, MIN_ASSET_RIGHTS_TO_READ),
      context,
    );

    if (matched_asset_ids.list.length === 0) {
      return {
        ids: [],
        objects: {
          assets: {},
          users: {},
        },
        total: 0,
      };
    }

    // Gather parents:
    const parent_ids = await getAllParentIdsOfAssetIdsTx(
      this.assetRepo.manager,
      matched_asset_ids.list,
    );

    // Load assets
    const all_ids = [...matched_asset_ids.list, ...parent_ids];

    const loaded_asset_map = await this.assetSelectorService.getShortsMap(
      {
        where: {
          id: all_ids,

          // NOTE: Выбираем с удаленными, на случай, если родитель был удален
          // А сами запрашиваемые записи были проверены на удаление раньше при запросе id
          withDeleted: true,
        },
      },
      context,
    );

    // Gather references
    const ref_map = await this.refService.getRefsByAssetIds(all_ids, context);

    //Get comments
    const asset_comments = await this.commentService.getCommentsByAssetIds(
      all_ids,
      context,
    );

    const assets: { [assetId: string]: AssetFullDTO } = {};
    const user_ids = new Set<number>();

    const blocks = await getAssetAllBlocksTx(
      this.assetRepo.manager,
      all_ids,
      context,
    );

    const gather_type_ids = (asset_id: string, type_ids: string[]) => {
      const asset = loaded_asset_map.get(asset_id);
      if (!asset) return;
      if (asset.parentIds && asset.parentIds.length > 0) {
        type_ids.push(asset.parentIds[0]);
        gather_type_ids(asset.parentIds[0], type_ids);
      }
    };

    // View attracting assets
    if (userView) {
      await this._viewAttractingAsset(
        context.projectId,
        [...loaded_asset_map.keys()],
        context.userId,
      );
    }

    for (const [assetId, asset] of loaded_asset_map) {
      const type_ids = [];
      gather_type_ids(assetId, type_ids); // NOTE: собираем актуальные типы

      assets[asset.id] = {
        ...asset,
        typeIds: type_ids,
        blocks: blocks
          .filter((block) => block.assetId === asset.id)
          .map((b) => this._convertAssetBlockToDTO(b)),
        comments: asset_comments.filter((ac) => ac.assetId === asset.id),
        references: ref_map.get(asset.id) ?? [],
      };
      user_ids.add(asset.creatorUserId);
    }

    const users = await this.userRepo
      .createQueryBuilder('u')
      .where('id = ANY(:user_ids)', { user_ids: [...user_ids] })
      .select(['u.id AS id', 'u.name as name'])
      .execute();

    const creators = {};
    for (const user of users) {
      creators[user.id] = { ...user };
    }
    return {
      ids: matched_asset_ids.list,
      objects: {
        assets,
        users: creators,
      },
      total: matched_asset_ids.total,
    };
  }

  async _viewAttractingAsset(
    project_id: string,
    asset_ids: string[],
    user_id: number,
  ) {
    await this.assetRepo.manager.transaction(async (tx) => {
      const notification_asset_ids: string[] = (
        (
          await tx.query(
            ` UPDATE notification_assets
          SET viewed_at = now()
          WHERE project_id = $1 AND asset_id = ANY ($2) AND user_id = $3 AND viewed_at IS NULL
          RETURNING notification_id
        `,
            [decodeBigNumberKey(project_id), asset_ids, user_id],
          )
        )[0] as { notification_id: string }[]
      ).map((n) => n.notification_id);
      if (notification_asset_ids.length > 0) {
        const left_not_viewed_notification_ids = (
          (await tx.query(
            `SELECT notification_id
           FROM notification_assets
           WHERE viewed_at IS NULL AND project_id = $1 AND
                user_id = $2 AND
                notification_id = ANY ($3)
           GROUP BY notification_id`,
            [decodeBigNumberKey(project_id), user_id, notification_asset_ids],
          )) as { notification_id: string }[]
        ).map((n) => n.notification_id);
        const need_view_notification_ids = notification_asset_ids.filter(
          (n_id) => !left_not_viewed_notification_ids.includes(n_id),
        );
        await tx.query(
          ` UPDATE notifications
            SET viewed_at = now()
            WHERE auto_view = TRUE AND notifications.project_id = $1 AND notifications.id = ANY ($2) AND user_id = $3          
          `,
          [decodeBigNumberKey(project_id), need_view_notification_ids, user_id],
        );
      }
    });
  }

  async getAssetsGraph(
    selection: AssetPropsSelectionBase,
    context: AssetSelectorContext,
  ): Promise<AssetsGraphResultDTO> {
    const asset_ids = await this.assetSelectorService.getIds(
      assignRightsFilterInSelection(
        {
          ...selection,
          count: undefined,
          offset: undefined,
        },
        MIN_ASSET_RIGHTS_TO_READ,
      ),
      context,
    );

    const limit = selection.count ? selection.count : 1000;
    const offset = selection.offset ? selection.offset : 0;
    let links = (await this.assetRepo.query(
      ` SELECT 
          asset_id as source, target_asset_id as target, link_type::varchar as type
          FROM asset_link_comps
          WHERE project_id = $1 AND
                (asset_id = ANY ($2) OR
                target_asset_id = ANY ($2)
              )
        UNION
          SELECT asset_id as source, target_asset_id as target, 'reference'
          FROM asset_references
          WHERE project_id = $1 AND (asset_id = ANY($2) OR target_asset_id = ANY($2))
        UNION
          SELECT id as source, unnest(parent_ids) target, 'inherit'
          FROM assets
          WHERE project_id = $1 AND id = ANY ($2)
        UNION
          SELECT id as source, parent_id as target, 'inherit'
          FROM (
              SELECT unnest(parent_ids) parent_id, id
              FROM assets
              WHERE project_id = $1 AND 
                    (parent_ids && $2)
          ) x
          WHERE parent_id = ANY ($2)
        LIMIT ${parseInt(limit as any) + 1}
        OFFSET ${parseInt(offset as any)}
  `,
      [decodeBigNumberKey(context.projectId), asset_ids],
    )) as { source: string; target: string; type: string }[];
    const more = links.length > limit;
    if (more) links = links.slice(0, limit);

    const linked_asset_ids = new Set<string>();
    for (const link of links) {
      linked_asset_ids.add(link.source);
      linked_asset_ids.add(link.target);
    }
    const loaded_asset_list = await this.assetSelectorService.getShorts(
      {
        where: { id: [...linked_asset_ids] },
      },
      context,
    );

    const assets = {};
    for (const asset of loaded_asset_list) {
      assets[asset.id] = this._convertAssetShortToDTO(asset);
    }

    return {
      list: links,
      objects: {
        assets,
      },
      more,
    };
  }

  async getDefaulTaskColumnId(
    context: AssetSelectorContext,
  ): Promise<string | null> {
    const col = await this.assetRepo.manager
      .createQueryBuilder(ColumnEntity, 'c')
      .where(
        `c.project_id = :project_id
          AND c.deleted_at IS NULL`,
        {
          project_id: decodeBigNumberKey(context.projectId),
        },
      )
      .orderBy('index')
      .select('id')
      .limit(1)
      .getRawOne();
    return col ? col.id : null;
  }

  async getNextTaskNum(context: AssetSelectorContext): Promise<number> {
    const max_num = await this.assetSelectorService.getView(
      {
        select: [
          {
            prop: 'basic||props|\\num',
            func: 'max',
            as: 'taskNum',
          },
        ],
        where: {
          typeids: TASK_ASSET_ID,
          issystem: false,
        },
      },
      context,
    );
    return max_num.length > 0 && max_num[0].taskNum
      ? parseInt(max_num[0].taskNum as any) + 1
      : 1;
  }

  async reorder(
    reorderParams: AssetReorderDTO,
    context: AssetSelectorContext,
  ): Promise<string[]> {
    const changers: AssetChanger[] = [];
    const avail_ids = new Set(
      await this.assetSelectorService.getIds(
        assignRightsFilterInSelection(
          {
            where: {
              id: reorderParams.ids,
            },
          },
          MIN_ASSET_RIGHTS_TO_INDEX,
        ),
        context,
      ),
    );
    const start_index = reorderParams.indexFrom ?? 1;
    let cur_index = start_index;
    const index_step =
      reorderParams.indexTo && avail_ids.size > 1
        ? (reorderParams.indexTo - start_index) / (avail_ids.size - 1)
        : 1;
    const affected_ids: string[] = [];
    for (const asset_id of reorderParams.ids) {
      if (avail_ids.has(asset_id)) {
        const changer = await AssetChanger.ChangeAssets(
          {
            ...context,
            tx: context.tx ?? this.assetRepo.manager,
          },
          {
            id: asset_id,
          },
          {
            main: {
              index: cur_index,
            },
          },
        );
        cur_index += index_step;
        changers.push(changer);
        affected_ids.push(asset_id);
      }
    }
    if (changers.length > 0) {
      await this.assetRepo.manager.transaction(async (tx) => {
        for (const changer of changers) {
          await changer.commit(tx);
        }
      });
    }
    return affected_ids;
  }

  async checkNeedNotification(
    change: AssetCreateDTO,
    context: AssetSelectorContext,
    asset_ids: string[],
  ) {
    const serv_inf = await this.assetSelectorService.getView(
      {
        select: [
          'id',
          'title',
          {
            prop: 'info||props|\\archivedat',
            as: 'archivedat',
          },
          {
            prop: 'info||props|\\iscompleted',
            as: 'iscompleted',
          },
        ],
        where: {
          id: asset_ids,
          typeids: TASK_ASSET_ID,
        },
      },
      context,
    );
    const serv_map = new Map<
      string,
      {
        id: string;
        title: string;
        //assignedto: number | null;
        archivedat: string | null;
        //taskcolumn: string | null;
        iscompleted: boolean;
        //result: string;
      }
    >();
    for (const serv of serv_inf) {
      serv_map.set(serv.id as any, serv as any);
    }

    for (const changed_task of serv_inf) {
      const info_props = change.props
        ? (change.props['info||props'] as any)
        : undefined;
      if (!info_props) continue;

      const task = serv_map.get(changed_task.id as any);
      if (!task) continue;
      if (task?.archivedat) {
        if (!(info_props['\\archivedat'] && !task.iscompleted)) {
          continue;
        }
      }
      const content: {
        old: any;
        new: any;
      } = {
        old: {},
        new: {},
      };
      for (const prop_title of Object.keys(info_props)) {
        content.new = {
          ...content.new,
          ...{ [prop_title]: info_props[prop_title] },
        };
      }
      await this.projectActionRepo.insert({
        id: uuidv4(),
        projectId: context.projectId,
        assetId: task.id,
        userId: context.userId,
        actionType: ActionType.TASK_CHANGE,
        content: JSON.stringify(content), // поля которые изменились
      });
    }
  }

  async getHistory(
    projectAccess: ProjectAccess,
    asset_id: string,
    filterData: AssetHistoryQueryDTO,
  ): Promise<ApiResultListWithMore<AssetHistoryDTO>> {
    const avail_ids = await this.assetSelectorService.getIds(
      assignRightsFilterInSelection(
        {
          where: {
            id: asset_id,
          },
        },
        MIN_ASSET_RIGHTS_TO_HISTORY,
      ),
      projectAccess,
    );
    if (avail_ids.length !== 1) {
      throw new ApiError(
        'No rights to get history',
        ApiErrorCodes.ACCESS_DENIED,
      );
    }

    const request_count = filterData.count ? filterData.count : 1000;
    const rows = await this.assetChangeAssetEntityRepo
      .createQueryBuilder('aca')
      .innerJoin(
        AssetChangeEntity,
        'ac',
        'aca.change_id = ac.id AND aca.project_id = ac.project_id',
      )
      .innerJoin(UserEntity, 'u', 'ac.user_id = u.id')
      .leftJoin(
        ProjectUserEntity,
        'pu',
        'pu.user_id = u.id AND pu.project_id = aca.project_id',
      )
      .where('aca.asset_id = :assetId AND aca.project_id = :projectId', {
        assetId: asset_id,
        projectId: decodeBigNumberKey(projectAccess.projectId),
      })
      .limit(request_count + 1)
      .orderBy('ac.created_at', 'DESC')
      .addSelect([
        'aca.undo_change as undo_change',
        'aca.redo_change as redo_change',
        'u.id as user_id',
        'COALESCE(pu.user_name, u.name) as user_name',
        'ac.id as id',
        'ac.created_at as created_at',
      ])
      .getRawMany<{
        undo_change: AssetCreateDTO;
        redo_change: AssetCreateDTO;
        user_id: number;
        user_name: string;
        id: string;
        created_at: Date;
      }>();

    const more = rows.length > request_count;

    return {
      list: rows.map((row) => {
        return {
          id: row.id,
          undo: row.undo_change,
          redo: row.redo_change,
          user: {
            accountId: row.user_id,
            name: row.user_name,
          },
          createdAt: row.created_at.toISOString(),
        };
      }),
      more,
    };
  }
}
