import { Injectable } from '@nestjs/common';
import { Kysely, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';

import { AlbumUserRole } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { asUuid } from 'src/utils/database';

type IActivityAccess = IAccessRepository['activity'];
type IAlbumAccess = IAccessRepository['album'];
type IAssetAccess = IAccessRepository['asset'];
type IAuthDeviceAccess = IAccessRepository['authDevice'];
type IMemoryAccess = IAccessRepository['memory'];
type IPersonAccess = IAccessRepository['person'];
type IPartnerAccess = IAccessRepository['partner'];
type IStackAccess = IAccessRepository['stack'];
type ITagAccess = IAccessRepository['tag'];
type ITimelineAccess = IAccessRepository['timeline'];

@Injectable()
class ActivityAccess implements IActivityAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
    if (activityIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('activity')
      .select('activity.id')
      .where('activity.id', 'in', [...activityIds])
      .where('activity.userId', '=', userId)
      .execute()
      .then((activities) => new Set(activities.map((activity) => activity.id)));
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
    if (activityIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('activity')
      .select('activity.id')
      .leftJoin('albums', (join) => join.onRef('activity.albumId', '=', 'albums.id').on('albums.deletedAt', 'is', null))
      .where('activity.id', 'in', [...activityIds])
      .whereRef('albums.ownerId', '=', asUuid(userId))
      .execute()
      .then((activities) => new Set(activities.map((activity) => activity.id)));
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
    if (albumIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('albums')
      .select('albums.id')
      .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id')
      .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null))
      .where('albums.id', 'in', [...albumIds])
      .where('albums.isActivityEnabled', '=', true)
      .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('users.id', '=', userId)]))
      .where('albums.deletedAt', 'is', null)
      .execute()
      .then((albums) => new Set(albums.map((album) => album.id)));
  }
}

class AlbumAccess implements IAlbumAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
    if (albumIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('albums')
      .select('albums.id')
      .where('albums.id', 'in', [...albumIds])
      .where('albums.ownerId', '=', userId)
      .where('albums.deletedAt', 'is', null)
      .execute()
      .then((albums) => new Set(albums.map((album) => album.id)));
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> {
    if (albumIds.size === 0) {
      return new Set();
    }

    const accessRole =
      access === AlbumUserRole.EDITOR ? [AlbumUserRole.EDITOR] : [AlbumUserRole.EDITOR, AlbumUserRole.VIEWER];

    return this.db
      .selectFrom('albums')
      .select('albums.id')
      .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id')
      .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null))
      .where('albums.id', 'in', [...albumIds])
      .where('albums.deletedAt', 'is', null)
      .where('users.id', '=', userId)
      .where('albumUsers.role', 'in', [...accessRole])
      .execute()
      .then((albums) => new Set(albums.map((album) => album.id)));
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>> {
    if (albumIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('shared_links')
      .select('shared_links.albumId')
      .where('shared_links.id', '=', sharedLinkId)
      .where('shared_links.albumId', 'in', [...albumIds])
      .execute()
      .then(
        (sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))),
      );
  }
}

class AssetAccess implements IAssetAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
    if (assetIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('albums')
      .innerJoin('albums_assets_assets as albumAssets', 'albums.id', 'albumAssets.albumsId')
      .innerJoin('assets', (join) =>
        join.onRef('assets.id', '=', 'albumAssets.assetsId').on('assets.deletedAt', 'is', null),
      )
      .leftJoin('albums_shared_users_users as albumUsers', 'albumUsers.albumsId', 'albums.id')
      .leftJoin('users', (join) => join.onRef('users.id', '=', 'albumUsers.usersId').on('users.deletedAt', 'is', null))
      .select(['assets.id', 'assets.livePhotoVideoId'])
      .where(
        sql`array["assets"."id", "assets"."livePhotoVideoId"]`,
        '&&',
        sql`array[${sql.join([...assetIds])}]::uuid[] `,
      )
      .where((eb) => eb.or([eb('albums.ownerId', '=', userId), eb('users.id', '=', userId)]))
      .where('albums.deletedAt', 'is', null)
      .execute()
      .then((assets) => {
        const allowedIds = new Set<string>();
        for (const asset of assets) {
          if (asset.id && assetIds.has(asset.id)) {
            allowedIds.add(asset.id);
          }
          if (asset.livePhotoVideoId && assetIds.has(asset.livePhotoVideoId)) {
            allowedIds.add(asset.livePhotoVideoId);
          }
        }
        return allowedIds;
      });
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
    if (assetIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('assets')
      .select('assets.id')
      .where('assets.id', 'in', [...assetIds])
      .where('assets.ownerId', '=', userId)
      .execute()
      .then((assets) => new Set(assets.map((asset) => asset.id)));
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
    if (assetIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('partners as partner')
      .innerJoin('users as sharedBy', (join) =>
        join.onRef('sharedBy.id', '=', 'partner.sharedById').on('sharedBy.deletedAt', 'is', null),
      )
      .innerJoin('assets', (join) =>
        join.onRef('assets.ownerId', '=', 'sharedBy.id').on('assets.deletedAt', 'is', null),
      )
      .select('assets.id')
      .where('partner.sharedWithId', '=', userId)
      .where('assets.isArchived', '=', false)
      .where('assets.id', 'in', [...assetIds])
      .execute()
      .then((assets) => new Set(assets.map((asset) => asset.id)));
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>> {
    if (assetIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('shared_links')
      .leftJoin('albums', (join) =>
        join.onRef('albums.id', '=', 'shared_links.albumId').on('albums.deletedAt', 'is', null),
      )
      .leftJoin('shared_link__asset', 'shared_link__asset.sharedLinksId', 'shared_links.id')
      .leftJoin('assets', (join) =>
        join.onRef('assets.id', '=', 'shared_link__asset.assetsId').on('assets.deletedAt', 'is', null),
      )
      .leftJoin('albums_assets_assets', 'albums_assets_assets.albumsId', 'albums.id')
      .leftJoin('assets as albumAssets', (join) =>
        join.onRef('albumAssets.id', '=', 'albums_assets_assets.assetsId').on('albumAssets.deletedAt', 'is', null),
      )
      .select([
        'assets.id as assetId',
        'assets.livePhotoVideoId as assetLivePhotoVideoId',
        'albumAssets.id as albumAssetId',
        'albumAssets.livePhotoVideoId as albumAssetLivePhotoVideoId',
      ])
      .where('shared_links.id', '=', sharedLinkId)
      .where(
        sql`array["assets"."id", "assets"."livePhotoVideoId", "albumAssets"."id", "albumAssets"."livePhotoVideoId"]`,
        '&&',
        sql`array[${sql.join([...assetIds])}]::uuid[] `,
      )
      .execute()
      .then((rows) => {
        const allowedIds = new Set<string>();
        for (const row of rows) {
          if (row.assetId && assetIds.has(row.assetId)) {
            allowedIds.add(row.assetId);
          }
          if (row.assetLivePhotoVideoId && assetIds.has(row.assetLivePhotoVideoId)) {
            allowedIds.add(row.assetLivePhotoVideoId);
          }
          if (row.albumAssetId && assetIds.has(row.albumAssetId)) {
            allowedIds.add(row.albumAssetId);
          }
          if (row.albumAssetLivePhotoVideoId && assetIds.has(row.albumAssetLivePhotoVideoId)) {
            allowedIds.add(row.albumAssetLivePhotoVideoId);
          }
        }
        return allowedIds;
      });
  }
}

class AuthDeviceAccess implements IAuthDeviceAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>> {
    if (deviceIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('sessions')
      .select('sessions.id')
      .where('sessions.userId', '=', userId)
      .where('sessions.id', 'in', [...deviceIds])
      .execute()
      .then((tokens) => new Set(tokens.map((token) => token.id)));
  }
}

class StackAccess implements IStackAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, stackIds: Set<string>): Promise<Set<string>> {
    if (stackIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('asset_stack as stacks')
      .select('stacks.id')
      .where('stacks.id', 'in', [...stackIds])
      .where('stacks.ownerId', '=', userId)
      .execute()
      .then((stacks) => new Set(stacks.map((stack) => stack.id)));
  }
}

class TimelineAccess implements ITimelineAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
    if (partnerIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('partners')
      .select('partners.sharedById')
      .where('partners.sharedById', 'in', [...partnerIds])
      .where('partners.sharedWithId', '=', userId)
      .execute()
      .then((partners) => new Set(partners.map((partner) => partner.sharedById)));
  }
}

class MemoryAccess implements IMemoryAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>> {
    if (memoryIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('memories')
      .select('memories.id')
      .where('memories.id', 'in', [...memoryIds])
      .where('memories.ownerId', '=', userId)
      .where('memories.deletedAt', 'is', null)
      .execute()
      .then((memories) => new Set(memories.map((memory) => memory.id)));
  }
}

class PersonAccess implements IPersonAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>> {
    if (personIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('person')
      .select('person.id')
      .where('person.id', 'in', [...personIds])
      .where('person.ownerId', '=', userId)
      .execute()
      .then((persons) => new Set(persons.map((person) => person.id)));
  }

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>): Promise<Set<string>> {
    if (assetFaceIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('asset_faces')
      .select('asset_faces.id')
      .leftJoin('assets', (join) =>
        join.onRef('assets.id', '=', 'asset_faces.assetId').on('assets.deletedAt', 'is', null),
      )
      .where('asset_faces.id', 'in', [...assetFaceIds])
      .where('assets.ownerId', '=', userId)
      .execute()
      .then((faces) => new Set(faces.map((face) => face.id)));
  }
}

class PartnerAccess implements IPartnerAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
    if (partnerIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('partners')
      .select('partners.sharedById')
      .where('partners.sharedById', 'in', [...partnerIds])
      .where('partners.sharedWithId', '=', userId)
      .execute()
      .then((partners) => new Set(partners.map((partner) => partner.sharedById)));
  }
}

class TagAccess implements ITagAccess {
  constructor(private db: Kysely<DB>) {}

  @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
  @ChunkedSet({ paramIndex: 1 })
  async checkOwnerAccess(userId: string, tagIds: Set<string>): Promise<Set<string>> {
    if (tagIds.size === 0) {
      return new Set();
    }

    return this.db
      .selectFrom('tags')
      .select('tags.id')
      .where('tags.id', 'in', [...tagIds])
      .where('tags.userId', '=', userId)
      .execute()
      .then((tags) => new Set(tags.map((tag) => tag.id)));
  }
}

export class AccessRepository implements IAccessRepository {
  activity: IActivityAccess;
  album: IAlbumAccess;
  asset: IAssetAccess;
  authDevice: IAuthDeviceAccess;
  memory: IMemoryAccess;
  person: IPersonAccess;
  partner: IPartnerAccess;
  stack: IStackAccess;
  tag: ITagAccess;
  timeline: ITimelineAccess;

  constructor(@InjectKysely() db: Kysely<DB>) {
    this.activity = new ActivityAccess(db);
    this.album = new AlbumAccess(db);
    this.asset = new AssetAccess(db);
    this.authDevice = new AuthDeviceAccess(db);
    this.memory = new MemoryAccess(db);
    this.person = new PersonAccess(db);
    this.partner = new PartnerAccess(db);
    this.stack = new StackAccess(db);
    this.tag = new TagAccess(db);
    this.timeline = new TimelineAccess(db);
  }
}