import { and, desc, eq, gte, inArray, isNull, or, sql } from "drizzle-orm";
import {
  hostAccess,
  hosts,
  roles,
  sharedHostSecrets,
  snippetAccess,
  snippets,
  users,
} from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
import { insertReturning } from "./returning.js";

export type RbacAccessTargetType = "user" | "role";

export interface RbacAccessListItem {
  id: number;
  targetType: RbacAccessTargetType;
  userId: string | null;
  roleId: number | null;
  username: string | null;
  roleName: string | null;
  roleDisplayName: string | null;
  grantedBy: string;
  grantedByUsername: string | null;
  permissionLevel: string;
  expiresAt: string | null;
  createdAt: string;
}

export interface RbacSharedHost {
  id: number;
  name: string | null;
  ip: string;
  port: number;
  username: string;
  folder: string | null;
  tags: string | null;
  permissionLevel: string;
  expiresAt: string | null;
  grantedBy: string;
  ownerUsername: string;
}

export interface RbacSharedSnippet {
  id: number;
  name: string;
  content: string;
  description: string | null;
  folder: string | null;
  ownerUsername: string;
  permissionLevel: string;
  expiresAt: string | null;
}

export interface RbacVisibleSharedSnippet extends RbacSharedSnippet {
  userId: string;
  order: number;
  createdAt: string;
  updatedAt: string;
  isNote: boolean;
}

export interface RbacAccessibleSnippet extends RbacVisibleSharedSnippet {
  hostFilter: string | null;
}

export interface RbacRoleHostAccessCredentialSource {
  hostAccessId: number;
  credentialId: number | null;
  rdpCredentialId: number | null;
  vncCredentialId: number | null;
  telnetCredentialId: number | null;
  hostId: number;
  hostOwnerId: string;
}

export interface RbacVisibleHostAccessEntry {
  hostId: number;
  permissionLevel: string;
  expiresAt: string | null;
}

export type RbacAccessTarget =
  | { targetType: "user"; targetUserId: string }
  | { targetType: "role"; targetRoleId: number };

export type UpsertHostAccessInput = RbacAccessTarget & {
  hostId: number;
  grantedBy: string;
  permissionLevel: string;
  expiresAt: string | null;
};

export type UpsertSnippetAccessInput = RbacAccessTarget & {
  snippetId: number;
  grantedBy: string;
  expiresAt: string | null;
};

type RawAccessListItem = Omit<RbacAccessListItem, "targetType">;

function toAccessListItem(access: RawAccessListItem): RbacAccessListItem {
  return {
    ...access,
    targetType: access.userId ? "user" : "role",
  };
}

export class RbacAccessRepository {
  constructor(
    private readonly context: DatabaseContext,
    private readonly onWrite?: () => void | Promise<void>,
  ) {}

  async listHostAccess(hostId: number): Promise<RbacAccessListItem[]> {
    const rows = await this.context.drizzle
      .select({
        id: hostAccess.id,
        userId: hostAccess.userId,
        roleId: hostAccess.roleId,
        username: users.username,
        roleName: roles.name,
        roleDisplayName: roles.displayName,
        grantedBy: hostAccess.grantedBy,
        grantedByUsername: sql<
          string | null
        >`(SELECT username FROM users WHERE id = ${hostAccess.grantedBy})`,
        permissionLevel: hostAccess.permissionLevel,
        expiresAt: hostAccess.expiresAt,
        createdAt: hostAccess.createdAt,
      })
      .from(hostAccess)
      .leftJoin(users, eq(hostAccess.userId, users.id))
      .leftJoin(roles, eq(hostAccess.roleId, roles.id))
      .where(eq(hostAccess.hostId, hostId))
      .orderBy(desc(hostAccess.createdAt));

    return rows.map(toAccessListItem);
  }

  async upsertHostAccess(input: UpsertHostAccessInput): Promise<{
    id: number;
    created: boolean;
  }> {
    const existing = await this.findHostAccess(input.hostId, input);

    if (existing) {
      await this.context.drizzle
        .update(hostAccess)
        .set({
          permissionLevel: input.permissionLevel,
          expiresAt: input.expiresAt,
        })
        .where(eq(hostAccess.id, existing.id));

      await this.afterWrite();
      return { id: existing.id, created: false };
    }

    const [created] = await insertReturning(this.context, hostAccess, {
      hostId: input.hostId,
      userId: input.targetType === "user" ? input.targetUserId : null,
      roleId: input.targetType === "role" ? input.targetRoleId : null,
      grantedBy: input.grantedBy,
      permissionLevel: input.permissionLevel,
      expiresAt: input.expiresAt,
    });

    await this.afterWrite();
    return { id: created.id, created: true };
  }

  async revokeHostAccess(accessId: number, hostId: number): Promise<void> {
    await this.context.drizzle
      .delete(hostAccess)
      .where(and(eq(hostAccess.id, accessId), eq(hostAccess.hostId, hostId)));
    await this.afterWrite();
  }

  async deleteHostAccessForHost(hostId: number): Promise<number> {
    const result = await this.context.drizzle
      .delete(hostAccess)
      .where(eq(hostAccess.hostId, hostId));

    if (rowsAffected(result) > 0) {
      await this.afterWrite();
    }

    return rowsAffected(result);
  }

  async deleteHostAccessForHosts(hostIds: number[]): Promise<number> {
    if (hostIds.length === 0) {
      return 0;
    }

    const result = await this.context.drizzle
      .delete(hostAccess)
      .where(inArray(hostAccess.hostId, hostIds));

    if (rowsAffected(result) > 0) {
      await this.afterWrite();
    }

    return rowsAffected(result);
  }

  async deleteHostAccessForUserReferences(userId: string): Promise<number> {
    const directResult = await this.context.drizzle
      .delete(hostAccess)
      .where(eq(hostAccess.userId, userId));

    const result = await this.context.drizzle
      .delete(hostAccess)
      .where(eq(hostAccess.grantedBy, userId));

    const deletedCount = rowsAffected(directResult) + rowsAffected(result);
    if (deletedCount > 0) {
      await this.afterWrite();
    }

    return deletedCount;
  }

  async findDirectHostAccess(
    hostId: number,
    userId: string,
  ): Promise<typeof hostAccess.$inferSelect | null> {
    const rows = await this.context.drizzle
      .select()
      .from(hostAccess)
      .where(and(eq(hostAccess.hostId, hostId), eq(hostAccess.userId, userId)))
      .limit(1);

    return rows[0] ?? null;
  }

  async listSnippetAccess(snippetId: number): Promise<RbacAccessListItem[]> {
    const rows = await this.context.drizzle
      .select({
        id: snippetAccess.id,
        userId: snippetAccess.userId,
        roleId: snippetAccess.roleId,
        username: users.username,
        roleName: roles.name,
        roleDisplayName: roles.displayName,
        grantedBy: snippetAccess.grantedBy,
        grantedByUsername: sql<
          string | null
        >`(SELECT username FROM users WHERE id = ${snippetAccess.grantedBy})`,
        permissionLevel: snippetAccess.permissionLevel,
        expiresAt: snippetAccess.expiresAt,
        createdAt: snippetAccess.createdAt,
      })
      .from(snippetAccess)
      .leftJoin(users, eq(snippetAccess.userId, users.id))
      .leftJoin(roles, eq(snippetAccess.roleId, roles.id))
      .where(eq(snippetAccess.snippetId, snippetId))
      .orderBy(desc(snippetAccess.createdAt));

    return rows.map(toAccessListItem);
  }

  async upsertSnippetAccess(input: UpsertSnippetAccessInput): Promise<{
    id: number;
    created: boolean;
  }> {
    const existing = await this.findSnippetAccess(input.snippetId, input);

    if (existing) {
      await this.context.drizzle
        .update(snippetAccess)
        .set({ expiresAt: input.expiresAt })
        .where(eq(snippetAccess.id, existing.id));

      await this.afterWrite();
      return { id: existing.id, created: false };
    }

    const [created] = await insertReturning(this.context, snippetAccess, {
      snippetId: input.snippetId,
      userId: input.targetType === "user" ? input.targetUserId : null,
      roleId: input.targetType === "role" ? input.targetRoleId : null,
      grantedBy: input.grantedBy,
      permissionLevel: "view",
      expiresAt: input.expiresAt,
    });

    await this.afterWrite();
    return { id: created.id, created: true };
  }

  async revokeSnippetAccess(
    accessId: number,
    snippetId: number,
  ): Promise<void> {
    await this.context.drizzle
      .delete(snippetAccess)
      .where(
        and(
          eq(snippetAccess.id, accessId),
          eq(snippetAccess.snippetId, snippetId),
        ),
      );
    await this.afterWrite();
  }

  async listSharedHosts(
    userId: string,
    roleIds: number[],
    now = new Date().toISOString(),
  ): Promise<RbacSharedHost[]> {
    return this.context.drizzle
      .select({
        id: hosts.id,
        name: hosts.name,
        ip: hosts.ip,
        port: hosts.port,
        username: hosts.username,
        folder: hosts.folder,
        tags: hosts.tags,
        permissionLevel: hostAccess.permissionLevel,
        expiresAt: hostAccess.expiresAt,
        grantedBy: hostAccess.grantedBy,
        ownerUsername: users.username,
      })
      .from(hostAccess)
      .innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
      .innerJoin(users, eq(hosts.userId, users.id))
      .where(
        and(
          this.userOrRoleHostAccessFilter(userId, roleIds),
          or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
        ),
      )
      .orderBy(desc(hostAccess.createdAt));
  }

  async listVisibleHostAccessEntries(
    userId: string,
    roleIds: number[],
    now = new Date().toISOString(),
  ): Promise<RbacVisibleHostAccessEntry[]> {
    return this.context.drizzle
      .select({
        hostId: hostAccess.hostId,
        permissionLevel: hostAccess.permissionLevel,
        expiresAt: hostAccess.expiresAt,
      })
      .from(hostAccess)
      .where(
        and(
          this.userOrRoleHostAccessFilter(userId, roleIds),
          or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
        ),
      )
      .orderBy(desc(hostAccess.createdAt));
  }

  async listSharedSnippets(
    userId: string,
    roleIds: number[],
    now = new Date().toISOString(),
  ): Promise<RbacSharedSnippet[]> {
    const directShared = await this.context.drizzle
      .select({
        id: snippets.id,
        name: snippets.name,
        content: snippets.content,
        description: snippets.description,
        folder: snippets.folder,
        ownerUsername: users.username,
        permissionLevel: snippetAccess.permissionLevel,
        expiresAt: snippetAccess.expiresAt,
      })
      .from(snippetAccess)
      .innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
      .innerJoin(users, eq(snippets.userId, users.id))
      .where(
        and(
          eq(snippetAccess.userId, userId),
          or(
            isNull(snippetAccess.expiresAt),
            gte(snippetAccess.expiresAt, now),
          ),
        ),
      );

    if (roleIds.length === 0) {
      return directShared;
    }

    const directIds = new Set(directShared.map((snippet) => snippet.id));
    const roleShared = await this.context.drizzle
      .select({
        id: snippets.id,
        name: snippets.name,
        content: snippets.content,
        description: snippets.description,
        folder: snippets.folder,
        ownerUsername: users.username,
        permissionLevel: snippetAccess.permissionLevel,
        expiresAt: snippetAccess.expiresAt,
      })
      .from(snippetAccess)
      .innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
      .innerJoin(users, eq(snippets.userId, users.id))
      .where(
        and(
          or(
            isNull(snippetAccess.expiresAt),
            gte(snippetAccess.expiresAt, now),
          ),
          inArray(snippetAccess.roleId, roleIds),
        ),
      );

    return [
      ...directShared,
      ...roleShared.filter((snippet) => !directIds.has(snippet.id)),
    ];
  }

  async listVisibleSharedSnippets(
    userId: string,
    roleIds: number[],
    now = new Date().toISOString(),
  ): Promise<RbacVisibleSharedSnippet[]> {
    return this.context.drizzle
      .select({
        id: snippets.id,
        userId: snippets.userId,
        name: snippets.name,
        content: snippets.content,
        description: snippets.description,
        folder: snippets.folder,
        order: snippets.order,
        createdAt: snippets.createdAt,
        updatedAt: snippets.updatedAt,
        isNote: snippets.isNote,
        ownerUsername: users.username,
        permissionLevel: snippetAccess.permissionLevel,
        expiresAt: snippetAccess.expiresAt,
      })
      .from(snippetAccess)
      .innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
      .innerJoin(users, eq(snippets.userId, users.id))
      .where(
        and(
          this.userOrRoleSnippetAccessFilter(userId, roleIds),
          or(
            isNull(snippetAccess.expiresAt),
            gte(snippetAccess.expiresAt, now),
          ),
        ),
      );
  }

  async findAccessibleSharedSnippet(
    snippetId: number,
    userId: string,
    roleIds: number[],
    now = new Date().toISOString(),
  ): Promise<RbacAccessibleSnippet | null> {
    const rows = await this.context.drizzle
      .select({
        id: snippets.id,
        userId: snippets.userId,
        name: snippets.name,
        content: snippets.content,
        description: snippets.description,
        folder: snippets.folder,
        order: snippets.order,
        createdAt: snippets.createdAt,
        updatedAt: snippets.updatedAt,
        hostFilter: snippets.hostFilter,
        isNote: snippets.isNote,
        ownerUsername: users.username,
        permissionLevel: snippetAccess.permissionLevel,
        expiresAt: snippetAccess.expiresAt,
      })
      .from(snippetAccess)
      .innerJoin(snippets, eq(snippetAccess.snippetId, snippets.id))
      .innerJoin(users, eq(snippets.userId, users.id))
      .where(
        and(
          eq(snippetAccess.snippetId, snippetId),
          this.userOrRoleSnippetAccessFilter(userId, roleIds),
          or(
            isNull(snippetAccess.expiresAt),
            gte(snippetAccess.expiresAt, now),
          ),
        ),
      )
      .limit(1);

    return rows[0] ?? null;
  }

  async deleteExpiredHostAccess(
    now = new Date().toISOString(),
  ): Promise<number> {
    const result = await this.context.drizzle
      .delete(hostAccess)
      .where(
        and(
          sql`${hostAccess.expiresAt} IS NOT NULL`,
          sql`${hostAccess.expiresAt} <= ${now}`,
        ),
      );

    if (rowsAffected(result) > 0) {
      await this.afterWrite();
    }

    return rowsAffected(result);
  }

  async findActiveHostAccess(
    hostId: number,
    userId: string,
    roleIds: number[],
    now = new Date().toISOString(),
  ): Promise<typeof hostAccess.$inferSelect | null> {
    const rows = await this.context.drizzle
      .select()
      .from(hostAccess)
      .where(
        and(
          eq(hostAccess.hostId, hostId),
          this.userOrRoleHostAccessFilter(userId, roleIds),
          or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
        ),
      )
      .limit(1);

    return rows[0] ?? null;
  }

  async touchHostAccess(
    accessId: number,
    lastAccessedAt = new Date().toISOString(),
  ): Promise<void> {
    await this.context.drizzle
      .update(hostAccess)
      .set({ lastAccessedAt })
      .where(eq(hostAccess.id, accessId));
    await this.afterWrite();
  }

  async listRoleHostAccessCredentialSources(
    roleId: number,
  ): Promise<RbacRoleHostAccessCredentialSource[]> {
    return this.context.drizzle
      .select({
        hostAccessId: hostAccess.id,
        credentialId: hosts.credentialId,
        rdpCredentialId: hosts.rdpCredentialId,
        vncCredentialId: hosts.vncCredentialId,
        telnetCredentialId: hosts.telnetCredentialId,
        hostId: hosts.id,
        hostOwnerId: hosts.userId,
      })
      .from(hostAccess)
      .innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
      .where(eq(hostAccess.roleId, roleId));
  }

  async findSharedSecretForHostUserProtocol(
    hostId: number,
    userId: string,
    protocol: string,
  ): Promise<typeof sharedHostSecrets.$inferSelect | null> {
    const rows = await this.context.drizzle
      .select({
        secret: sharedHostSecrets,
      })
      .from(sharedHostSecrets)
      .innerJoin(hostAccess, eq(sharedHostSecrets.hostAccessId, hostAccess.id))
      .where(
        and(
          eq(hostAccess.hostId, hostId),
          eq(sharedHostSecrets.targetUserId, userId),
          eq(sharedHostSecrets.protocol, protocol),
        ),
      )
      .limit(1);

    return rows[0]?.secret ?? null;
  }

  async listActiveHostAccessGrants(
    hostId: number,
    now = new Date().toISOString(),
  ): Promise<(typeof hostAccess.$inferSelect)[]> {
    return this.context.drizzle
      .select()
      .from(hostAccess)
      .where(
        and(
          eq(hostAccess.hostId, hostId),
          or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
        ),
      );
  }

  async findHostAccessById(
    accessId: number,
    hostId: number,
  ): Promise<typeof hostAccess.$inferSelect | null> {
    const rows = await this.context.drizzle
      .select()
      .from(hostAccess)
      .where(and(eq(hostAccess.id, accessId), eq(hostAccess.hostId, hostId)))
      .limit(1);

    return rows[0] ?? null;
  }

  async updateHostAccessGrant(
    accessId: number,
    hostId: number,
    update: { permissionLevel?: string; expiresAt?: string | null },
  ): Promise<boolean> {
    const result = await this.context.drizzle
      .update(hostAccess)
      .set(update)
      .where(and(eq(hostAccess.id, accessId), eq(hostAccess.hostId, hostId)));

    if (rowsAffected(result) > 0) {
      await this.afterWrite();
    }

    return rowsAffected(result) > 0;
  }

  async findHostAccessOwnerId(hostAccessId: number): Promise<string | null> {
    const rows = await this.context.drizzle
      .select({ ownerId: hosts.userId })
      .from(hostAccess)
      .innerJoin(hosts, eq(hostAccess.hostId, hosts.id))
      .where(eq(hostAccess.id, hostAccessId))
      .limit(1);

    return rows[0]?.ownerId ?? null;
  }

  private userOrRoleHostAccessFilter(userId: string, roleIds: number[]) {
    if (roleIds.length === 0) {
      return eq(hostAccess.userId, userId);
    }

    return or(
      eq(hostAccess.userId, userId),
      inArray(hostAccess.roleId, roleIds),
    );
  }

  private userOrRoleSnippetAccessFilter(userId: string, roleIds: number[]) {
    if (roleIds.length === 0) {
      return eq(snippetAccess.userId, userId);
    }

    return or(
      eq(snippetAccess.userId, userId),
      inArray(snippetAccess.roleId, roleIds),
    );
  }

  private async findHostAccess(hostId: number, target: RbacAccessTarget) {
    const rows = await this.context.drizzle
      .select()
      .from(hostAccess)
      .where(
        and(
          eq(hostAccess.hostId, hostId),
          target.targetType === "user"
            ? eq(hostAccess.userId, target.targetUserId)
            : eq(hostAccess.roleId, target.targetRoleId),
        ),
      )
      .limit(1);

    return rows[0] ?? null;
  }

  private async findSnippetAccess(snippetId: number, target: RbacAccessTarget) {
    const rows = await this.context.drizzle
      .select()
      .from(snippetAccess)
      .where(
        and(
          eq(snippetAccess.snippetId, snippetId),
          target.targetType === "user"
            ? eq(snippetAccess.userId, target.targetUserId)
            : eq(snippetAccess.roleId, target.targetRoleId),
        ),
      )
      .limit(1);

    return rows[0] ?? null;
  }

  private async afterWrite(): Promise<void> {
    await this.onWrite?.();
  }
}
