import assert from 'assert';
import { Op } from 'sequelize';
import db from '@/db/models';
import services from '@/services/file';
import ValidationError from '@/shared/errors/validation';
import type { DbApiOptions, FileInput, FileRelation } from '@/db/api/types';

class FileDBApi {
  static async replaceRelationFiles(
    relation: FileRelation,
    rawFiles: FileInput | FileInput[] | null | undefined,
    options?: DbApiOptions,
  ): Promise<void> {
    assert(relation.belongsTo, 'belongsTo is required');
    assert(relation.belongsToColumn, 'belongsToColumn is required');
    assert(relation.belongsToId, 'belongsToId is required');

    const files: FileInput[] = Array.isArray(rawFiles)
      ? rawFiles
      : rawFiles
        ? [rawFiles]
        : [];

    await this._removeLegacyFiles(relation, files, options);
    await this._addFiles(relation, files, options);
  }

  static async _addFiles(
    relation: FileRelation,
    files: FileInput[],
    options?: DbApiOptions,
  ): Promise<void> {
    const transaction = options?.transaction;
    const currentUser = options?.currentUser ?? { id: null };

    const inexistentFiles = files.filter((file) => !!file.new);

    for (const file of inexistentFiles) {
      if (!file.name || !file.publicUrl) {
        throw new ValidationError('iam.errors.fileNameRequired');
      }

      await db.file.create(
        {
          belongsTo: relation.belongsTo,
          belongsToColumn: relation.belongsToColumn,
          belongsToId: relation.belongsToId,
          name: file.name,
          sizeInBytes: file.sizeInBytes ?? null,
          privateUrl: file.privateUrl ?? null,
          publicUrl: file.publicUrl,
          createdById: currentUser.id,
          updatedById: currentUser.id,
        },
        { transaction },
      );
    }
  }

  static async _removeLegacyFiles(
    relation: FileRelation,
    files: FileInput[],
    options?: DbApiOptions,
  ): Promise<void> {
    const transaction = options?.transaction;

    const filesToDelete = await db.file.findAll({
      where: {
        belongsTo: relation.belongsTo,
        belongsToId: relation.belongsToId,
        belongsToColumn: relation.belongsToColumn,
        id: {
          [Op.notIn]: files
            .filter((file) => !file.new)
            .map((file) => file.id)
            .filter((id): id is string => Boolean(id)),
        },
      },
      transaction,
    });

    for (const file of filesToDelete) {
      if (file.privateUrl) {
        await services.deleteGCloud(file.privateUrl);
      }
      await file.destroy({ transaction });
    }
  }

  /**
   * Resolves the owning organization of a stored file by its `privateUrl`, via
   * the uploader (`createdById`). Used by the download ownership check
   * (Workstream 3 §3.5 / file workstream) so a file cannot be fetched
   * cross-tenant by guessing its path. `found: false` means no tracked file
   * matches the path (callers deny by default).
   */
  static async findOwnerOrganizationIdByPrivateUrl(
    privateUrl: string,
  ): Promise<{ found: boolean; organizationId: string | null }> {
    const row = await db.file.findOne({
      where: { privateUrl },
      attributes: ['id', 'createdById'],
    });
    if (!row) {
      return { found: false, organizationId: null };
    }
    if (!row.createdById) {
      return { found: true, organizationId: null };
    }
    const creator = await db.users.findByPk(row.createdById, {
      attributes: ['organizationId'],
    });
    return { found: true, organizationId: creator?.organizationId ?? null };
  }
}

export default FileDBApi;
