import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository, SelectQueryBuilder } from 'typeorm';
import { ApiError } from '../common/error/api-error';
import { ApiErrorCodes } from '../common/error/api-error-codes';
import { v4 as uuidv4 } from 'uuid';
import { WorkspaceEntity } from '../entities/workspace.entity';
import {
  ChangeWorkspaceDTO,
  CreateWorkspaceDTO,
  WorkspaceDTO,
  WorkspaceQueryDTO,
} from './dto/workspace-dto';
import { ApiResultListWithTotal } from 'src/common/types/api-result';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { decodeBigNumberKey } from '../utils/big-number-key';
import {
  AssetScope,
  AssetScopeFromId,
  AssetScopeToId,
  SYSTEM_PROJECT_ID,
} from '../constants';
import { AccessTag, ProjectAccess } from '../utils/project-access';
import {
  AssetRights,
  MIN_WORKSPACE_RIGHTS_TO_DELETE,
  MIN_WORKSPACE_RIGHTS_TO_READ,
} from '../asset/logic/Rights';
import { WorkspaceReorderDTO } from './dto/workspace-reorder-dto';

@Injectable()
export class WorkspaceService {
  constructor(
    @InjectRepository(WorkspaceEntity)
    private readonly workspaceRepo: Repository<WorkspaceEntity>,
    @InjectDataSource() private readonly connection: Connection,
  ) {}

  private async _checkCanCreateWorkspaceOrThrow(
    projectAccess: ProjectAccess,
    parent_id: string | null,
  ) {
    if (projectAccess.accessTags.has(AccessTag.ROOT)) {
      return;
    }
    if (!projectAccess.userRole) {
      throw new ApiError(
        'No rights to create workspace',
        ApiErrorCodes.ACCESS_DENIED,
      );
    }
  }

  private async _checkCanChangeWorkspaceOrThrow(
    projectAccess: ProjectAccess,
    workspace_id: string,
  ) {
    if (projectAccess.accessTags.has(AccessTag.ROOT)) {
      return;
    }
    if (!projectAccess.userRole) {
      throw new ApiError(
        'No rights to change workspace',
        ApiErrorCodes.ACCESS_DENIED,
      );
    }
  }

  async checkAndCreateWorkspace(
    workspace: CreateWorkspaceDTO,
    projectAccess: ProjectAccess,
  ): Promise<WorkspaceEntity & { rights: number }> {
    await this._checkCanCreateWorkspaceOrThrow(
      projectAccess,
      workspace.parentId ?? null,
    );

    const workspace_id = uuidv4();
    await this.doCreateWorkspaceWithId(
      projectAccess.projectId,
      workspace_id,
      workspace,
    );

    const created_workspace = await this._getProjectWorkspace(
      projectAccess.projectId,
      workspace_id,
    );
    const rights = projectAccess.userRole
      ? MIN_WORKSPACE_RIGHTS_TO_DELETE
      : MIN_WORKSPACE_RIGHTS_TO_READ;
    return {
      ...created_workspace,
      rights,
    };
  }

  async doCreateWorkspaceWithId(
    project_id: string,
    workspace_id: string,
    workspace: CreateWorkspaceDTO,
  ): Promise<void> {
    if (workspace.parentId) {
      await this._hasProjectWorkspace(project_id, workspace.parentId);
    }

    const new_workspace: QueryDeepPartialEntity<WorkspaceEntity> & {
      id: string;
    } = {
      id: workspace_id,
      title: workspace.title,
      parentId: workspace.parentId,
      projectId: project_id,
      index: workspace.index ?? null,
    };

    if (workspace.scope) {
      const scopeId = AssetScopeToId.get(workspace.scope);
      if (!scopeId) {
        throw new ApiError(
          'Unknown workspace scope',
          ApiErrorCodes.PARAM_BAD_VALUE,
          {
            value: workspace.scope,
          },
        );
      }

      new_workspace.scopeId = scopeId;
    }

    await this.workspaceRepo.insert(new_workspace);
  }

  async doChangeWorkspace(
    project_id: string,
    workspace_id: string,
    props: ChangeWorkspaceDTO,
  ): Promise<void> {
    const changes: any = {};
    if (props.parentId !== undefined) {
      if (workspace_id === props.parentId) {
        throw new ApiError(
          `Workspace with id = ${workspace_id} can't be placed inside itself`,
          ApiErrorCodes.ACTION_NOT_AVAILABLE,
        );
      }
      if (props.parentId !== null) {
        await this._hasProjectWorkspace(project_id, props.parentId);

        const child_workspace = await this.connection.query(
          `
          SELECT *
          FROM workspaces_nesting
          WHERE id = $1 AND parent_id = $2`,
          [props.parentId, workspace_id],
        );
        if (child_workspace.length) {
          throw new ApiError(
            `Destination workspace with id = ${props.parentId} is a child workspace`,
            ApiErrorCodes.ENTITY_NOT_FOUND,
          );
        }
      }
      changes.parentId = props.parentId ? props.parentId : undefined;
    }

    if (props.title) {
      changes.title = props.title;
    }
    if (props.index !== undefined) {
      changes.index = props.index;
    }

    await this.workspaceRepo
      .createQueryBuilder()
      .update(WorkspaceEntity)
      .where(`id = :id AND project_id = :project_id`, {
        id: workspace_id,
        project_id: decodeBigNumberKey(project_id),
      })
      .set(changes)
      .execute();
  }

  async changeWorkspace(
    workspace_id: string,
    props: ChangeWorkspaceDTO,
    projectAccess: ProjectAccess,
  ): Promise<WorkspaceEntity & { rights: number }> {
    await this._checkCanChangeWorkspaceOrThrow(projectAccess, workspace_id);
    await this._hasProjectWorkspace(projectAccess.projectId, workspace_id);

    await this.doChangeWorkspace(projectAccess.projectId, workspace_id, props);

    const workspace = await this._hasProjectWorkspace(
      projectAccess.projectId,
      workspace_id,
    );
    const rights = projectAccess.userRole
      ? MIN_WORKSPACE_RIGHTS_TO_DELETE
      : MIN_WORKSPACE_RIGHTS_TO_READ;
    return {
      ...workspace,
      rights,
    };
  }

  async deleteWorkspace(
    workspace_id: string,
    projectAccess: ProjectAccess,
  ): Promise<void> {
    await this._checkCanChangeWorkspaceOrThrow(projectAccess, workspace_id);
    await this._hasProjectWorkspace(projectAccess.projectId, workspace_id);

    await this.connection.query(
      `
    UPDATE workspaces
    SET deleted_at = NOW()
    WHERE id IN (
      SELECT id
      FROM workspaces_nesting
      WHERE parent_id = $1 )`,
      [workspace_id],
    );
  }

  private _applyWorkspaceRightsToQuery(
    projectAccess: ProjectAccess,
    query: SelectQueryBuilder<WorkspaceEntity>,
  ) {
    if (projectAccess.accessTags.has(AccessTag.ROOT)) return;
    if (!projectAccess.userRole) {
      if (projectAccess.projectParams.isPublicGdd) {
        query.andWhere('w.scope_id = :scope_id', {
          scope_id: AssetScopeToId.get(AssetScope.DEFAULT),
        });
      } else {
        query.andWhere('1 <> 1'); // NOTE: нет доступа - возвращаем пустоту
      }
    } else if (
      projectAccess.userRole.defaultWorkspaceRights === AssetRights.NO
    ) {
      query.innerJoin(
        'workspace_rights',
        'wr',
        'wr.project_id = w.project_id AND wr.workspace_id = w.id AND wr.rights > 0',
      );
    }
  }

  async getWorkspacesOfProject(
    params: WorkspaceQueryDTO,
    projectAccess: ProjectAccess,
  ): Promise<ApiResultListWithTotal<WorkspaceDTO>> {
    const dbquery = this.workspaceRepo
      .createQueryBuilder('w')
      .where(
        '(w.project_id = :project_id OR w.project_id = :system_project_id) AND w.deleted_at IS NULL',
        {
          project_id: decodeBigNumberKey(projectAccess.projectId),
          system_project_id: SYSTEM_PROJECT_ID,
        },
      );
    this._applyWorkspaceRightsToQuery(projectAccess, dbquery);

    if (params.where?.workspaceId) {
      dbquery.andWhere('(w.parent_id = :workspace_id)', {
        workspace_id: `${params.where.workspaceId}`,
      });
    } else if (params.where?.workspaceId === '') {
      dbquery.andWhere('(w.parent_id IS NULL)');
    }

    if (params.where?.ids && Array.isArray(params.where?.ids)) {
      dbquery.andWhere('(w.id = ANY(:ids))', {
        ids: params.where?.ids,
      });
    }

    if (params.where?.scope) {
      const scope_id = AssetScopeToId.get(
        params.where?.scope.toString().toLowerCase(),
      );
      if (!scope_id) {
        throw new ApiError(
          'Unknown asset scope',
          ApiErrorCodes.PARAM_BAD_VALUE,
          {
            value: params.where?.scope,
          },
        );
      }
      dbquery.andWhere('w.scope_id = :scope_id', {
        scope_id,
      });
    }

    dbquery.orderBy('w.title', 'ASC');
    dbquery.addOrderBy('w.created_at', 'ASC');
    dbquery.addOrderBy('w.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();

    const rights = projectAccess.userRole
      ? MIN_WORKSPACE_RIGHTS_TO_DELETE
      : MIN_WORKSPACE_RIGHTS_TO_READ;

    return {
      list: list.map((w) => {
        return {
          id: w.id,
          createdAt: w.createdAt,
          parentId: w.parentId,
          title: w.title,
          updatedAt: w.updatedAt,
          index: w.index,
          scope: AssetScopeFromId.get(w.scopeId),
          rights,
        };
      }),
      total,
    };
  }

  private async _hasProjectWorkspace(
    project_id: string,
    workspace_id: string,
  ): Promise<WorkspaceEntity> {
    const workspace = await this._getProjectWorkspace(project_id, workspace_id);
    if (!workspace) {
      throw new ApiError(
        `Workspace with id = ${workspace_id} doesn't exist in project ${project_id}`,
        ApiErrorCodes.ENTITY_NOT_FOUND,
      );
    }
    return workspace;
  }

  private async _getProjectWorkspace(
    project_id: string,
    workspace_id: string,
  ): Promise<WorkspaceEntity> {
    return await this.workspaceRepo.findOne({
      id: workspace_id,
      projectId: project_id,
      deletedAt: null,
    });
  }

  async reorder(
    reorderParams: WorkspaceReorderDTO,
    projectAccess: ProjectAccess,
  ): Promise<string[]> {
    const avail_workspaces = await this.getWorkspacesOfProject(
      {
        where: {
          ids: reorderParams.ids,
        },
      },
      projectAccess,
    );

    const avail_ids = new Set(avail_workspaces.list.map((w) => w.id));
    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[] = [];
    if (avail_ids.size > 0) {
      await this.workspaceRepo.manager.transaction(async (tx) => {
        for (const asset_id of reorderParams.ids) {
          if (avail_ids.has(asset_id)) {
            await tx.update(
              WorkspaceEntity,
              {
                id: asset_id,
              },
              {
                index: cur_index,
              },
            );
            cur_index += index_step;
            affected_ids.push(asset_id);
          }
        }
      });
    }
    return affected_ids;
  }
}
