import { and, asc, eq } from "drizzle-orm";
import { termixIdentities, termixIdentityKeys } from "../db/schema.js";
import type { DatabaseContext } from "./database-context.js";
import { rowsAffected } from "./mutation-result.js";
import { insertReturning, updateReturning } from "./returning.js";

export type TermixIdentityRecord = typeof termixIdentities.$inferSelect;
export type NewTermixIdentityRecord = typeof termixIdentities.$inferInsert;
export type TermixIdentityUpdate = Partial<
  Pick<NewTermixIdentityRecord, "handle" | "description" | "updatedAt">
>;

export type TermixIdentityKeyRecord = typeof termixIdentityKeys.$inferSelect;
export type NewTermixIdentityKeyRecord = typeof termixIdentityKeys.$inferInsert;
export type TermixIdentityKeyUpdate = Partial<
  Pick<NewTermixIdentityKeyRecord, "enabled" | "label">
>;

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

  async findIdentityForUser(
    userId: string,
  ): Promise<TermixIdentityRecord | null> {
    const rows = await this.context.drizzle
      .select()
      .from(termixIdentities)
      .where(eq(termixIdentities.userId, userId))
      .limit(1);

    return rows[0] ?? null;
  }

  async findIdentityByHandle(
    handle: string,
  ): Promise<TermixIdentityRecord | null> {
    const rows = await this.context.drizzle
      .select()
      .from(termixIdentities)
      .where(eq(termixIdentities.handle, handle))
      .limit(1);

    return rows[0] ?? null;
  }

  async isHandleTaken(handle: string): Promise<boolean> {
    const rows = await this.context.drizzle
      .select({ id: termixIdentities.id })
      .from(termixIdentities)
      .where(eq(termixIdentities.handle, handle))
      .limit(1);

    return rows.length > 0;
  }

  async createIdentity(
    identity: NewTermixIdentityRecord,
  ): Promise<TermixIdentityRecord> {
    const rows = await insertReturning(
      this.context,
      termixIdentities,
      identity,
    );

    await this.afterWrite();
    return rows[0];
  }

  async updateIdentityForUser(
    userId: string,
    update: TermixIdentityUpdate,
  ): Promise<TermixIdentityRecord | null> {
    const rows = await updateReturning(
      this.context,
      termixIdentities,
      update,
      eq(termixIdentities.userId, userId),
    );

    if (rows.length > 0) {
      await this.afterWrite();
    }

    return rows[0] ?? null;
  }

  async deleteIdentityForUser(userId: string): Promise<boolean> {
    const result = await this.context.drizzle
      .delete(termixIdentities)
      .where(eq(termixIdentities.userId, userId));

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

    return rowsAffected(result) > 0;
  }

  async deleteByUserId(userId: string): Promise<{
    identitiesDeleted: number;
    keysDeleted: number;
  }> {
    const keyResult = await this.context.drizzle
      .delete(termixIdentityKeys)
      .where(eq(termixIdentityKeys.userId, userId));

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

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

    return {
      identitiesDeleted: rowsAffected(result),
      keysDeleted: rowsAffected(keyResult),
    };
  }

  async listKeysByIdentityId(
    identityId: number,
  ): Promise<TermixIdentityKeyRecord[]> {
    return this.context.drizzle
      .select()
      .from(termixIdentityKeys)
      .where(eq(termixIdentityKeys.identityId, identityId))
      .orderBy(asc(termixIdentityKeys.id));
  }

  async listEnabledKeysByIdentityId(
    identityId: number,
  ): Promise<TermixIdentityKeyRecord[]> {
    return this.context.drizzle
      .select()
      .from(termixIdentityKeys)
      .where(
        and(
          eq(termixIdentityKeys.identityId, identityId),
          eq(termixIdentityKeys.enabled, true),
        ),
      )
      .orderBy(asc(termixIdentityKeys.id));
  }

  async listLinkedCredentialIds(identityId: number): Promise<number[]> {
    const rows = await this.context.drizzle
      .select({ credentialId: termixIdentityKeys.credentialId })
      .from(termixIdentityKeys)
      .where(
        and(
          eq(termixIdentityKeys.identityId, identityId),
          eq(termixIdentityKeys.enabled, true),
        ),
      );

    return Array.from(
      new Set(
        rows
          .map((row) => row.credentialId)
          .filter(
            (credentialId): credentialId is number => credentialId !== null,
          ),
      ),
    );
  }

  async createKey(
    key: NewTermixIdentityKeyRecord,
  ): Promise<TermixIdentityKeyRecord> {
    const rows = await insertReturning(this.context, termixIdentityKeys, key);

    await this.afterWrite();
    return rows[0];
  }

  async updateKeyForUser(
    userId: string,
    id: number,
    update: TermixIdentityKeyUpdate,
  ): Promise<TermixIdentityKeyRecord | null> {
    const rows = await updateReturning(
      this.context,
      termixIdentityKeys,
      update,
      and(eq(termixIdentityKeys.id, id), eq(termixIdentityKeys.userId, userId)),
    );

    if (rows.length > 0) {
      await this.afterWrite();
    }

    return rows[0] ?? null;
  }

  async deleteKeyForUser(userId: string, id: number): Promise<boolean> {
    const result = await this.context.drizzle
      .delete(termixIdentityKeys)
      .where(
        and(
          eq(termixIdentityKeys.id, id),
          eq(termixIdentityKeys.userId, userId),
        ),
      );

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

    return rowsAffected(result) > 0;
  }

  async findKeyForUser(
    userId: string,
    id: number,
  ): Promise<TermixIdentityKeyRecord | null> {
    const rows = await this.context.drizzle
      .select()
      .from(termixIdentityKeys)
      .where(
        and(
          eq(termixIdentityKeys.id, id),
          eq(termixIdentityKeys.userId, userId),
        ),
      )
      .limit(1);

    return rows[0] ?? null;
  }

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