import { Injectable } from '@nestjs/common';
import { and, eq, inArray, or } from 'drizzle-orm';
import { DatabaseService } from '../../common/database/database.service';
import { appState, userModuleAccess, users } from '../../database/schema';
import type { ChannelVisibility } from './community.repository';

const PUSH_STATE_KEY = 'ck_community_push_v1';
const MAX_DEVICES_PER_USER = 8;

export interface StoredPushSubscription {
  endpoint: string;
  expirationTime: number | null;
  keys: {
    p256dh: string;
    auth: string;
  };
}

interface PushState {
  version: 1;
  subscriptions: StoredPushSubscription[];
}

@Injectable()
export class CommunityPushRepository {
  constructor(private readonly database: DatabaseService) {}

  async subscriptionsForUser(ownerId: string): Promise<StoredPushSubscription[]> {
    const [row] = await this.database.db
      .select({ value: appState.value })
      .from(appState)
      .where(and(eq(appState.ownerId, ownerId), eq(appState.key, PUSH_STATE_KEY)))
      .limit(1);
    return parsePushState(row?.value).subscriptions;
  }

  async save(ownerId: string, subscription: StoredPushSubscription): Promise<void> {
    // A browser endpoint represents one physical app installation. If another
    // account logs in on that device, move the endpoint instead of leaving it
    // attached to both accounts (which could otherwise duplicate or leak an
    // admin-channel notification).
    const rows = await this.database.db
      .select({ ownerId: appState.ownerId, value: appState.value })
      .from(appState)
      .where(eq(appState.key, PUSH_STATE_KEY));
    for (const row of rows) {
      if (row.ownerId === ownerId) continue;
      const previous = parsePushState(row.value).subscriptions;
      const remaining = previous.filter((entry) => entry.endpoint !== subscription.endpoint);
      if (remaining.length !== previous.length) {
        await this.write(row.ownerId, { version: 1, subscriptions: remaining });
      }
    }

    const current = await this.subscriptionsForUser(ownerId);
    const subscriptions = [
      ...current.filter((entry) => entry.endpoint !== subscription.endpoint),
      subscription,
    ].slice(-MAX_DEVICES_PER_USER);
    await this.write(ownerId, { version: 1, subscriptions });
  }

  async remove(ownerId: string, endpoint: string): Promise<void> {
    const current = await this.subscriptionsForUser(ownerId);
    const subscriptions = current.filter((entry) => entry.endpoint !== endpoint);
    if (subscriptions.length === current.length) return;
    await this.write(ownerId, { version: 1, subscriptions });
  }

  /**
   * Active Community readers only. Administrators have implicit access and no
   * entitlement row; regular accounts need READ or WRITE. Admin-only channel
   * notifications never leave the administrator role.
   */
  async recipients(visibility: ChannelVisibility) {
    const access = visibility === 'ADMINS'
      ? eq(users.role, 'ADMIN')
      : or(eq(users.role, 'ADMIN'), inArray(userModuleAccess.level, ['READ', 'WRITE']));

    const rows = await this.database.db
      .select({ ownerId: appState.ownerId, value: appState.value })
      .from(appState)
      .innerJoin(users, eq(users.id, appState.ownerId))
      .leftJoin(
        userModuleAccess,
        and(
          eq(userModuleAccess.userId, users.id),
          eq(userModuleAccess.module, 'community'),
        ),
      )
      .where(
        and(
          eq(appState.key, PUSH_STATE_KEY),
          eq(users.isActive, true),
          access,
        ),
      );

    return rows.flatMap((row) =>
      parsePushState(row.value).subscriptions.map((subscription) => ({
        ownerId: row.ownerId,
        subscription,
      })),
    );
  }

  private async write(ownerId: string, value: PushState): Promise<void> {
    await this.database.db
      .insert(appState)
      .values({ ownerId, key: PUSH_STATE_KEY, value, updatedAt: new Date() })
      .onConflictDoUpdate({
        target: [appState.ownerId, appState.key],
        set: { value, updatedAt: new Date() },
      });
  }
}

function parsePushState(value: unknown): PushState {
  if (!value || typeof value !== 'object') return { version: 1, subscriptions: [] };
  const raw = (value as { subscriptions?: unknown }).subscriptions;
  if (!Array.isArray(raw)) return { version: 1, subscriptions: [] };

  const subscriptions = raw.filter(isStoredPushSubscription).slice(-MAX_DEVICES_PER_USER);
  return { version: 1, subscriptions };
}

function isStoredPushSubscription(value: unknown): value is StoredPushSubscription {
  if (!value || typeof value !== 'object') return false;
  const row = value as Partial<StoredPushSubscription>;
  return Boolean(
    typeof row.endpoint === 'string' &&
      row.endpoint.startsWith('https://') &&
      row.keys &&
      typeof row.keys.p256dh === 'string' &&
      typeof row.keys.auth === 'string',
  );
}
