import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { decodeBigNumberKey } from '../utils/big-number-key';
import {
  AssetSelectorContext,
  AssetSelectorService,
} from './asset-selector.service';
import { AssetPropWhere, AssetPropWhereOpKind } from './logic/PropsWhere';
import { AssetRefDTO, AssetsRefResultDTO } from './dto/asset-reference-dto';
import { AssetRefEntity } from '../entities/asset-ref.entity';
import {
  AssetPropValueAccount,
  castAssetPropValueToInt,
  castAssetPropValueToString,
  isFilledAssetPropValue,
  parseAssetBlockKeyRef,
  stringifyAssetBlockRef,
} from './logic/Props';
import { AssetBlockKeyEntity } from '../entities/asset-block-key.entity';
import { AssetPropAccountDTO } from './dto/block-dto';
import { getAssetBlockKeyTx, registerAssetBlockKeyTx } from './block-helpers';
import { assignRightsFilterInSelection } from './logic/assignRightsFilterInSelection';
import { MIN_ASSET_RIGHTS_TO_READ } from './logic/Rights';
import { ApiResultListWithTotal } from '../common/types/api-result';
import { AssetPropsSelectionBase } from './logic/PropsSelection';
import { QueryPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { TASK_ASSET_ID } from '../constants';
import { ProjectImportArchiveAssetReference } from '../project/dto/project-import.dto';

@Injectable()
export class RefService {
  constructor(
    private readonly assetSelectorService: AssetSelectorService,
    @InjectRepository(AssetRefEntity)
    private readonly refRepo: Repository<AssetRefEntity>,
  ) {}

  async getRefs(
    selection: AssetPropsSelectionBase,
    context: AssetSelectorContext,
  ): Promise<AssetsRefResultDTO> {
    const asset_ids_paginated = await this.assetSelectorService.getIdsPaginated(
      assignRightsFilterInSelection(
        {
          ...selection,
        },
        MIN_ASSET_RIGHTS_TO_READ,
      ),
      context,
    );
    if (asset_ids_paginated.list.length === 0) {
      return {
        ids: [],
        refs: {},
        total: asset_ids_paginated.total,
      };
    }

    const map = await this.getRefsByAssetIds(asset_ids_paginated.list, context);
    return {
      ids: asset_ids_paginated.list,
      refs: Object.fromEntries(map.entries()),
      total: asset_ids_paginated.total,
    };
  }

  async getShortsByAssetIds(
    asset_ids: string[],
    context: AssetSelectorContext,
  ): Promise<Map<string, ProjectImportArchiveAssetReference[]>> {
    const res_map = new Map<string, ProjectImportArchiveAssetReference[]>();
    if (asset_ids.length === 0) {
      return res_map;
    }
    const ref_ents = await (context.tx ? context.tx : this.refRepo.manager)
      .createQueryBuilder(AssetRefEntity, 'r')
      .where('r.asset_id = ANY(:asset_ids) and r.project_id = :project_id', {
        asset_ids: asset_ids,
        project_id: decodeBigNumberKey(context.projectId),
      })
      .orderBy('r.created_at')
      .getMany();
    const block_keys = new Set<string>();
    const target_asset_ids = new Set<string>();
    for (const ref_ent of ref_ents) {
      if (ref_ent.blockKey) block_keys.add(ref_ent.blockKey);
      if (ref_ent.targetBlockKey) block_keys.add(ref_ent.targetBlockKey);
      target_asset_ids.add(ref_ent.targetAssetId);
    }
    const block_keys_ents_map = new Map<string, AssetBlockKeyEntity>();
    if (block_keys.size > 0) {
      const block_keys_ents = await (context.tx
        ? context.tx
        : this.refRepo.manager
      )
        .createQueryBuilder(AssetBlockKeyEntity, 'bk')
        .where(
          `
        bk.project_id = :project_id AND bk.block_key = ANY(:block_keys)
      `,
          {
            project_id: decodeBigNumberKey(context.projectId),
            block_keys: [...block_keys],
          },
        )
        .getMany();
      for (const bke of block_keys_ents) {
        block_keys_ents_map.set(bke.blockKey, bke);
      }
    }

    for (const ref_ent of ref_ents) {
      let res = res_map.get(ref_ent.assetId);
      if (!res) {
        res = [];
        res_map.set(ref_ent.assetId, res);
      }

      const source_block_key_ent = ref_ent.blockKey
        ? block_keys_ents_map.get(ref_ent.blockKey)
        : undefined;
      const target_block_key_ent = ref_ent.targetBlockKey
        ? block_keys_ents_map.get(ref_ent.targetBlockKey)
        : undefined;

      res.push({
        sourceBlockRef: source_block_key_ent
          ? stringifyAssetBlockRef(
              source_block_key_ent.blockName,
              source_block_key_ent.blockTitle,
              source_block_key_ent.blockType,
            )
          : null,
        targetAssetId: ref_ent.targetAssetId,
        targetBlockRef: target_block_key_ent
          ? stringifyAssetBlockRef(
              target_block_key_ent.blockName,
              target_block_key_ent.blockTitle,
              target_block_key_ent.blockType,
            )
          : null,
      });
    }
    return res_map;
  }

  async getRefsByAssetIds(
    asset_ids: string[],
    context: AssetSelectorContext,
  ): Promise<Map<string, AssetRefDTO[]>> {
    const res_map = new Map<string, AssetRefDTO[]>();
    if (asset_ids.length === 0) {
      return res_map;
    }
    const ref_ents = await (context.tx ? context.tx : this.refRepo.manager)
      .createQueryBuilder(AssetRefEntity, 'r')
      .where('r.asset_id = ANY(:asset_ids) and r.project_id = :project_id', {
        asset_ids: asset_ids,
        project_id: decodeBigNumberKey(context.projectId),
      })
      .orderBy('r.created_at')
      .getMany();
    const block_keys = new Set<string>();
    const target_asset_ids = new Set<string>();
    for (const ref_ent of ref_ents) {
      if (ref_ent.blockKey) block_keys.add(ref_ent.blockKey);
      if (ref_ent.targetBlockKey) block_keys.add(ref_ent.targetBlockKey);
      target_asset_ids.add(ref_ent.targetAssetId);
    }
    const block_keys_ents_map = new Map<string, AssetBlockKeyEntity>();
    if (block_keys.size > 0) {
      const block_keys_ents = await (context.tx
        ? context.tx
        : this.refRepo.manager
      )
        .createQueryBuilder(AssetBlockKeyEntity, 'bk')
        .where(
          `
        bk.project_id = :project_id AND bk.block_key = ANY(:block_keys)
      `,
          {
            project_id: decodeBigNumberKey(context.projectId),
            block_keys: [...block_keys],
          },
        )
        .getMany();
      for (const bke of block_keys_ents) {
        block_keys_ents_map.set(bke.blockKey, bke);
      }
    }

    const target_info_where: AssetPropWhere = {
      id: [...target_asset_ids],
    };

    if (!context.userRole && !context.projectParams.isPublicTasks) {
      target_info_where.typeids = {
        op: AssetPropWhereOpKind.EQUAL_NOT,
        v: TASK_ASSET_ID,
      };
    }

    const target_infos = await this.assetSelectorService.getView(
      {
        where: target_info_where,
        select: ['id', 'title', 'typeids', 'name'],
      },
      context,
    );
    const target_infos_map = new Map<string, any>();
    for (const ti of target_infos) {
      target_infos_map.set(ti.id as string, ti);
    }

    for (const ref_ent of ref_ents) {
      const target_info = target_infos_map.get(ref_ent.targetAssetId);
      if (!target_info) {
        continue;
      }

      let res = res_map.get(ref_ent.assetId);
      if (!res) {
        res = [];
        res_map.set(ref_ent.assetId, res);
      }

      const source_block_key_ent = ref_ent.blockKey
        ? block_keys_ents_map.get(ref_ent.blockKey)
        : undefined;
      const target_block_key_ent = ref_ent.targetBlockKey
        ? block_keys_ents_map.get(ref_ent.targetBlockKey)
        : undefined;

      res.push({
        sourceBlockRef: source_block_key_ent
          ? stringifyAssetBlockRef(
              source_block_key_ent.blockName,
              source_block_key_ent.blockTitle,
              source_block_key_ent.blockType,
            )
          : null,
        targetAssetId: ref_ent.targetAssetId,
        targetBlockRef: target_block_key_ent
          ? stringifyAssetBlockRef(
              target_block_key_ent.blockName,
              target_block_key_ent.blockTitle,
              target_block_key_ent.blockType,
            )
          : null,
        targetTitle: target_info.title,
        targetName: target_info.name,
        createdAt: ref_ent.createdAt.toISOString(),
        targetTypeIds: target_info.typeids.map((p) => {
          return target_info[`typeids\\${p}`] as string;
        }),
      });
    }
    return res_map;
  }

  async doCreateRefs(
    tx: EntityManager,
    projectId: string,
    assetIds: string[],
    sourceBlockRef: string | null,
    targetAssetId: string,
    targetBlockRef: string | null,
  ) {
    const source_block_ref_parsed = sourceBlockRef
      ? parseAssetBlockKeyRef(sourceBlockRef)
      : null;
    const source_block_key = source_block_ref_parsed
      ? await registerAssetBlockKeyTx(
          tx,
          projectId,
          source_block_ref_parsed.blockName,
          source_block_ref_parsed.blockTitle,
          source_block_ref_parsed.blockType,
        )
      : null;

    const target_block_ref_parsed = targetBlockRef
      ? parseAssetBlockKeyRef(targetBlockRef)
      : null;
    const target_block_key = target_block_ref_parsed
      ? await registerAssetBlockKeyTx(
          tx,
          projectId,
          target_block_ref_parsed.blockName,
          target_block_ref_parsed.blockTitle,
          target_block_ref_parsed.blockType,
        )
      : null;
    const creating_refs: QueryPartialEntity<AssetRefEntity>[] = [];
    for (const asset_id of assetIds) {
      const ref: QueryPartialEntity<AssetRefEntity> = {
        projectId: projectId,
        assetId: asset_id,
        targetAssetId: targetAssetId,
        blockKey: source_block_key ?? '0',
        targetBlockKey: target_block_key ?? '0',
      };
      creating_refs.push(ref);
    }

    const do_staff = async (tx: EntityManager) => {
      await tx
        .createQueryBuilder(AssetRefEntity, 'ar')
        .insert()
        .values(creating_refs)
        .orIgnore()
        .execute();
    };

    if (tx.queryRunner && tx.queryRunner.isTransactionActive) {
      await do_staff(tx);
    } else {
      await tx.transaction(do_staff);
    }
  }

  async createRefs(
    where: AssetPropWhere,
    sourceBlockRef: string | null,
    targetAssetId: string,
    targetBlockRef: string | null,
    context: AssetSelectorContext,
  ): Promise<AssetsRefResultDTO> {
    const existen_ids = await this.assetSelectorService.getIds(
      {
        where: {
          ...where,
          isSystem: false,
        },
      },
      context,
    );
    if (existen_ids.length === 0) {
      return {
        ids: [],
        refs: {},
        total: 0,
      };
    }

    await this.doCreateRefs(
      context.tx ?? this.refRepo.manager,
      context.projectId,
      existen_ids,
      sourceBlockRef,
      targetAssetId,
      targetBlockRef,
    );

    return this.getRefs({ where }, context);
  }

  async deleteRef(
    where: AssetPropWhere,
    sourceBlockRef: string | null,
    targetAssetId: string,
    targetBlockRef: string | null,
    context: AssetSelectorContext,
  ): Promise<string[]> {
    const existen_ids = await this.assetSelectorService.getIds(
      {
        where: {
          ...where,
          isSystem: false,
        },
      },
      context,
    );
    if (existen_ids.length === 0) return [];

    const source_block_ref_parsed = sourceBlockRef
      ? parseAssetBlockKeyRef(sourceBlockRef)
      : null;
    const source_block_key = source_block_ref_parsed
      ? await getAssetBlockKeyTx(
          context.tx ?? this.refRepo.manager,
          context.projectId,
          source_block_ref_parsed.blockName,
          source_block_ref_parsed.blockTitle,
          source_block_ref_parsed.blockType,
        )
      : null;

    const target_block_ref_parsed = targetBlockRef
      ? parseAssetBlockKeyRef(targetBlockRef)
      : null;
    const target_block_key = target_block_ref_parsed
      ? await getAssetBlockKeyTx(
          context.tx ?? this.refRepo.manager,
          context.projectId,
          target_block_ref_parsed.blockName,
          target_block_ref_parsed.blockTitle,
          target_block_ref_parsed.blockType,
        )
      : null;
    await (context.tx ? context.tx : this.refRepo.manager)
      .createQueryBuilder()
      .delete()
      .from(AssetRefEntity)
      .where(
        `
        project_id = :project_id
        AND asset_id = ANY(:asset_ids)
        AND block_key = :block_key
        AND target_asset_id = :target_asset_id
        AND target_block_key = :target_block_key
    `,
        {
          project_id: decodeBigNumberKey(context.projectId),
          asset_ids: existen_ids,
          block_key: source_block_key ?? '0',
          target_asset_id: targetAssetId,
          target_block_key: target_block_key ?? '0',
        },
      )
      .execute();
  }
}
