import { TEAM_ROLES, TEAMS } from '@pig/core';
import type { Database, PlatformSettings } from '@pig/db';
import { invites, platformSettings, teamMemberships, users } from '@pig/db';
import { and, desc, eq, isNull } from 'drizzle-orm';
import { Hono } from 'hono';
import { createHash, randomBytes } from 'node:crypto';
import { z } from 'zod';
import { requireCapability } from '../lib/auth';
import type { Config } from '../lib/config';
import {
  apiError,
  bodylessMutation,
  MutationError,
  mutation,
  type ApiEnv,
} from '../lib/mutation';
import {
  decryptSecret,
  encryptionReady,
  encryptSecret,
  SecretConfigurationError,
} from '../lib/secrets';

const SETTINGS_ID = 'default';

export function normaliseInferenceEndpoint(value: string): string {
  return value.replace(/\/+$/, '');
}

export function isInferenceEndpoint(value: string): boolean {
  try {
    return new URL(value).hostname !== 'api.primeintellect.ai';
  } catch {
    return false;
  }
}

export const platformSettingsSchema = z
  .object({
    piggyModel: z.string().trim().min(1).max(200).optional(),
    piggyInferenceBase: z
      .string()
      .url()
      .transform(normaliseInferenceEndpoint)
      .refine(isInferenceEndpoint, 'Inference must not use the Prime compute API host.')
      .optional(),
    piggyEnabled: z.boolean().optional(),
    primeApiKey: z.string().trim().min(16).max(1000).optional(),
    clearPrimeApiKey: z.boolean().optional(),
    primeSyncEnabled: z.boolean().optional(),
    primeSyncIntervalMinutes: z.number().int().min(1).max(1440).optional(),
  })
  .strict()
  .refine((value) => Object.values(value).some((item) => item !== undefined), 'No settings supplied.')
  .refine(
    (value) => !(value.primeApiKey && value.clearPrimeApiKey),
    'A credential cannot be set and cleared together.',
  );

export const inviteCreateSchema = z
  .object({
    email: z.string().trim().email().max(200).optional(),
    team: z.enum(TEAMS).optional(),
    role: z.enum(TEAM_ROLES).default('member'),
    expiresAt: z.string().datetime().optional(),
    usesRemaining: z.number().int().min(1).max(100).default(1),
  })
  .strict();

export const memberAccessSchema = z
  .object({
    isPlatformAdmin: z.boolean(),
    memberships: z
      .array(
        z.object({
          team: z.enum(TEAMS),
          role: z.enum(TEAM_ROLES),
        }),
      )
      .max(TEAMS.length)
      .refine(
        (memberships) => new Set(memberships.map(({ team }) => team)).size === memberships.length,
        'Each team may appear only once.',
      ),
  })
  .strict();

function initialSettings(config: Config) {
  return {
    id: SETTINGS_ID,
    piggyModel: config.PIGGY_MODEL,
    piggyInferenceBase: normaliseInferenceEndpoint(config.PIGGY_INFERENCE_BASE),
    piggyEnabled: config.PIGGY_ENABLED,
    primeSyncEnabled: config.PRIME_SYNC_ENABLED,
    primeSyncIntervalMinutes: config.PRIME_SYNC_INTERVAL_MINUTES,
  };
}

export async function ensurePlatformSettings(config: Config, db: Database): Promise<PlatformSettings> {
  await db.insert(platformSettings).values(initialSettings(config)).onConflictDoNothing();
  const [row] = await db
    .select()
    .from(platformSettings)
    .where(eq(platformSettings.id, SETTINGS_ID))
    .limit(1);
  if (!row) throw new Error('Platform settings row is missing');
  return row;
}

export function platformSettingsResponse(row: PlatformSettings, config: Config) {
  const storedCredential = Boolean(row.primeApiKeyEncrypted);
  const environmentCredential = Boolean(config.PRIME_API_KEY);
  return {
    piggyModel: row.piggyModel,
    piggyInferenceBase: row.piggyInferenceBase,
    piggyEnabled: row.piggyEnabled,
    primeComputeBase: config.PRIME_API_BASE,
    primeApiKey: {
      configured: storedCredential || environmentCredential,
      source: storedCredential ? 'database' : environmentCredential ? 'environment' : null,
      updatedAt: storedCredential ? row.primeApiKeyUpdatedAt : null,
      encryptionReady: encryptionReady(config.PIG_SETTINGS_ENCRYPTION_KEY),
    },
    primeSyncEnabled: row.primeSyncEnabled,
    primeSyncIntervalMinutes: row.primeSyncIntervalMinutes,
    updatedAt: row.updatedAt,
  };
}

export async function effectivePrimeSyncConfig(config: Config, db: Database): Promise<Config> {
  const row = await ensurePlatformSettings(config, db);
  const storedKey = row.primeApiKeyEncrypted
    ? decryptSecret(row.primeApiKeyEncrypted, config.PIG_SETTINGS_ENCRYPTION_KEY)
    : undefined;
  return {
    ...config,
    PRIME_API_KEY: storedKey ?? config.PRIME_API_KEY,
    PRIME_SYNC_ENABLED: row.primeSyncEnabled,
    PRIME_SYNC_INTERVAL_MINUTES: row.primeSyncIntervalMinutes,
  };
}

type InviteRow = typeof invites.$inferSelect;

export function inviteMetadata(row: InviteRow, now = new Date()) {
  const status = row.revokedAt
    ? 'revoked'
    : row.usesRemaining < 1
      ? 'used'
      : row.expiresAt && row.expiresAt <= now
        ? 'expired'
        : 'active';
  return {
    id: row.id,
    email: row.email,
    team: row.team,
    role: row.role,
    usesRemaining: row.usesRemaining,
    expiresAt: row.expiresAt,
    redeemedAt: row.redeemedAt,
    revokedAt: row.revokedAt,
    createdAt: row.createdAt,
    status,
  };
}

function generateInviteCode(): string {
  return `pig_inv_${randomBytes(24).toString('base64url')}`;
}

export function createAdminSettingsRoutes(
  config: Config,
  db: Database,
  onSettingsChanged?: () => Promise<void>,
) {
  const app = new Hono<ApiEnv>();

  app.get('/api/admin/settings', async (c) => {
    requireCapability(c.get('principal'), 'settings:admin');
    return c.json(platformSettingsResponse(await ensurePlatformSettings(config, db), config));
  });

  const updateSettings = mutation(db, {
    schema: platformSettingsSchema,
    permission: { capability: 'settings:admin' },
    invalidMessage: 'Invalid platform settings.',
    async mutate({ input, principal, tx, now }) {
      const set: Partial<typeof platformSettings.$inferInsert> = {
        updatedAt: now,
        updatedByUserId: principal.userId,
      };
      if (input.piggyModel !== undefined) set.piggyModel = input.piggyModel;
      if (input.piggyInferenceBase !== undefined) set.piggyInferenceBase = input.piggyInferenceBase;
      if (input.piggyEnabled !== undefined) set.piggyEnabled = input.piggyEnabled;
      if (input.primeSyncEnabled !== undefined) set.primeSyncEnabled = input.primeSyncEnabled;
      if (input.primeSyncIntervalMinutes !== undefined) {
        set.primeSyncIntervalMinutes = input.primeSyncIntervalMinutes;
      }
      if (input.primeApiKey !== undefined) {
        try {
          set.primeApiKeyEncrypted = encryptSecret(
            input.primeApiKey,
            config.PIG_SETTINGS_ENCRYPTION_KEY,
          );
        } catch (error) {
          if (error instanceof SecretConfigurationError) {
            throw new MutationError('encryption_unavailable', error.message, 409);
          }
          throw error;
        }
        set.primeApiKeyUpdatedAt = now;
      } else if (input.clearPrimeApiKey) {
        set.primeApiKeyEncrypted = null;
        set.primeApiKeyUpdatedAt = null;
      }

      const [updated] = await tx
        .insert(platformSettings)
        .values({ ...initialSettings(config), ...set })
        .onConflictDoUpdate({ target: platformSettings.id, set })
        .returning();
      if (!updated) throw new Error('Platform settings update returned no row');

      return {
        data: platformSettingsResponse(updated, config),
        activity: {
          type: 'agent_action',
          subject: 'Updated platform settings',
          meta: {
            action: 'platform_settings.updated',
            fields: Object.keys(input),
            primeApiKeyChanged: input.primeApiKey !== undefined || Boolean(input.clearPrimeApiKey),
          },
        },
      };
    },
  });

  app.patch('/api/admin/settings', async (c, next) => {
    const response = await updateSettings(c, next);
    if (response?.ok && onSettingsChanged) {
      void onSettingsChanged().catch((error: unknown) =>
        console.error(
          '[pig] platform settings saved but runtime reload failed:',
          error instanceof Error ? error.message : 'unknown error',
        ),
      );
    }
    return response;
  });

  app.get('/api/admin/invites', async (c) => {
    requireCapability(c.get('principal'), 'settings:admin');
    const rows = await db.select().from(invites).orderBy(desc(invites.createdAt));
    return c.json(rows.map((row) => inviteMetadata(row)));
  });

  app.post(
    '/api/admin/invites',
    mutation(db, {
      schema: inviteCreateSchema,
      permission: { capability: 'settings:admin' },
      invalidMessage: 'Invalid invite.',
      async mutate({ input, principal, tx, now }) {
        const expiresAt = input.expiresAt ? new Date(input.expiresAt) : null;
        if (expiresAt && expiresAt <= now) {
          throw new MutationError('invalid_expiry', 'Invite expiry must be in the future.', 400);
        }
        const code = generateInviteCode();
        const [created] = await tx
          .insert(invites)
          .values({
            codeHash: createHash('sha256').update(code).digest('hex'),
            email: input.email?.toLowerCase(),
            team: input.team,
            role: input.role,
            expiresAt,
            usesRemaining: input.usesRemaining,
            createdByUserId: principal.userId,
            scopeNote: 'admin-ui',
          })
          .returning();
        if (!created) throw new Error('Invite insert returned no row');
        return {
          data: { ...inviteMetadata(created, now), code },
          activity: {
            type: 'agent_action',
            subject: `Created${created.email ? ` invite for ${created.email}` : ' workspace invite'}`,
            meta: {
              action: 'invite.created',
              inviteId: created.id,
              email: created.email,
              team: created.team,
              role: created.role,
              uses: created.usesRemaining,
            },
          },
        };
      },
    }),
  );

  app.delete(
    '/api/admin/invites/:id',
    bodylessMutation(db, {
      schema: z.object({}).strict(),
      permission: { capability: 'settings:admin' },
      invalidMessage: 'Invalid invite revocation.',
      async mutate({ params, tx, now }) {
        const id = params.id;
        if (!id) throw MutationError.notFound('Invite');
        const [existing] = await tx.select().from(invites).where(eq(invites.id, id)).limit(1);
        if (!existing) throw MutationError.notFound('Invite');
        const [updated] = existing.revokedAt
          ? [existing]
          : await tx.update(invites).set({ revokedAt: now }).where(eq(invites.id, id)).returning();
        if (!updated) throw MutationError.notFound('Invite');
        return {
          data: inviteMetadata(updated, now),
          activity: {
            type: 'agent_action',
            subject: `Revoked${existing.email ? ` invite for ${existing.email}` : ' workspace invite'}`,
            meta: {
              action: 'invite.revoked',
              inviteId: existing.id,
              alreadyRevoked: existing.revokedAt !== null,
            },
          },
        };
      },
    }),
  );

  app.get('/api/admin/members', async (c) => {
    requireCapability(c.get('principal'), 'settings:admin');
    const [people, memberships] = await Promise.all([
      db.select().from(users).where(isNull(users.deactivatedAt)).orderBy(users.name),
      db.select().from(teamMemberships),
    ]);
    return c.json(
      people.map((person) => ({
        id: person.id,
        name: person.name,
        email: person.email,
        title: person.title,
        isPlatformAdmin:
          person.isPlatformAdmin || config.adminEmails.includes(person.email.toLowerCase()),
        adminSource: config.adminEmails.includes(person.email.toLowerCase())
          ? 'environment'
          : person.isPlatformAdmin
            ? 'database'
            : null,
        memberships: memberships
          .filter(({ userId }) => userId === person.id)
          .map(({ team, role }) => ({ team, role })),
      })),
    );
  });

  app.patch(
    '/api/admin/members/:id/access',
    mutation(db, {
      schema: memberAccessSchema,
      permission: { capability: 'settings:admin' },
      invalidMessage: 'Invalid member access.',
      async mutate({ input, principal, params, tx, now }) {
        const id = params.id;
        if (!id) throw MutationError.notFound('Member');
        const [person] = await tx.select().from(users).where(eq(users.id, id)).limit(1);
        if (!person || person.deactivatedAt) throw MutationError.notFound('Member');
        const environmentAdmin = config.adminEmails.includes(person.email.toLowerCase());
        if (!input.isPlatformAdmin && environmentAdmin) {
          throw new MutationError(
            'environment_admin',
            'This administrator is pinned by PIG_ADMIN_EMAILS and must be changed in deployment configuration.',
            409,
          );
        }
        if (!input.isPlatformAdmin && id === principal.userId) {
          throw new MutationError(
            'self_demotion',
            'Ask another platform administrator to remove your access.',
            409,
          );
        }

        await tx
          .update(users)
          .set({ isPlatformAdmin: input.isPlatformAdmin, updatedAt: now })
          .where(eq(users.id, id));
        await tx.delete(teamMemberships).where(eq(teamMemberships.userId, id));
        if (input.memberships.length > 0) {
          await tx.insert(teamMemberships).values(
            input.memberships.map((membership, index) => ({
              userId: id,
              ...membership,
              isPrimary: index === 0,
            })),
          );
        }
        const data = {
          id,
          name: person.name,
          email: person.email,
          title: person.title,
          isPlatformAdmin: input.isPlatformAdmin || environmentAdmin,
          adminSource: environmentAdmin ? 'environment' : input.isPlatformAdmin ? 'database' : null,
          memberships: input.memberships,
        };
        return {
          data,
          activity: {
            type: 'agent_action',
            subject: `Updated access for ${person.name}`,
            meta: {
              action: 'member_access.updated',
              targetUserId: id,
              isPlatformAdmin: data.isPlatformAdmin,
              memberships: input.memberships,
            },
          },
        };
      },
    }),
  );

  return app;
}
