import { getServerById } from '../cqrs';
import { getDataSource } from '../db/database';
import {
  PluginDataEntity,
  ServerPluginEventDefinitionEntity,
  ServerPluginEventDirection,
  ServerPluginEventScope,
  ServerPluginRequirementEntity,
  ServerPluginRequirementStatus
} from '../entities';
import { findServerMembership } from './server-access.service';
import { resolveServerPermission } from './server-permissions.service';

export const DEFAULT_PLUGIN_EVENT_MAX_PAYLOAD_BYTES = 64 * 1024;

const VALID_REQUIREMENT_STATUSES = new Set<ServerPluginRequirementStatus>([
  'required',
  'optional',
  'recommended',
  'blocked',
  'incompatible'
]);
const VALID_EVENT_DIRECTIONS = new Set<ServerPluginEventDirection>([
  'clientToServer',
  'serverRelay',
  'p2pHint'
]);
const VALID_EVENT_SCOPES = new Set<ServerPluginEventScope>([
  'server',
  'channel',
  'user',
  'plugin'
]);
const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{1,126}[a-z0-9]$/;
const EVENT_NAME_PATTERN = /^[a-z][a-z0-9.:-]{1,126}[a-z0-9]$/;
const DATA_KEY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
const DATA_SCOPE_PATTERN = /^[a-zA-Z][a-zA-Z0-9._:-]{0,63}$/;

export interface PluginRequirementSummary {
  installUrl?: string;
  manifest?: unknown;
  pluginId: string;
  reason?: string;
  sourceUrl?: string;
  status: ServerPluginRequirementStatus;
  updatedAt: number;
  versionRange?: string;
}

export interface PluginEventDefinitionSummary {
  direction: ServerPluginEventDirection;
  eventName: string;
  maxPayloadBytes: number;
  pluginId: string;
  scope: ServerPluginEventScope;
  schemaJson?: string;
  updatedAt: number;
}

export interface PluginRequirementsSnapshot {
  eventDefinitions: PluginEventDefinitionSummary[];
  requirements: PluginRequirementSummary[];
  serverId: string;
  updatedAt: number;
}

export interface PluginDataRecord {
  key: string;
  ownerId?: string;
  pluginId: string;
  schemaVersion: number;
  scope: string;
  serverId: string;
  updatedAt: number;
  updatedBy?: string;
  value: unknown;
}

export interface PluginEventEnvelope {
  eventId?: string;
  eventName: string;
  payload: unknown;
  pluginId: string;
  serverId: string;
  sourcePluginUserId?: string;
  type: 'plugin_event';
}

export class PluginSupportError extends Error {
  constructor(
    readonly status: number,
    readonly code: string,
    message: string
  ) {
    super(message);
    this.name = 'PluginSupportError';
  }
}

function requirementRepository() {
  return getDataSource().getRepository(ServerPluginRequirementEntity);
}

function eventDefinitionRepository() {
  return getDataSource().getRepository(ServerPluginEventDefinitionEntity);
}

function pluginDataRepository() {
  return getDataSource().getRepository(PluginDataEntity);
}

function normalizeOptionalString(value: unknown, maxLength: number): string | null {
  if (typeof value !== 'string') {
    return null;
  }

  const normalized = value.trim();

  return normalized ? normalized.slice(0, maxLength) : null;
}

function assertPattern(value: string, pattern: RegExp, code: string, label: string): void {
  if (!pattern.test(value)) {
    throw new PluginSupportError(400, code, `Invalid ${label}`);
  }
}

function normalizePluginId(pluginId: unknown): string {
  const normalized = normalizeOptionalString(pluginId, 128);

  if (!normalized) {
    throw new PluginSupportError(400, 'MISSING_PLUGIN_ID', 'Missing plugin id');
  }

  assertPattern(normalized, PLUGIN_ID_PATTERN, 'INVALID_PLUGIN_ID', 'plugin id');
  return normalized;
}

function normalizeEventName(eventName: unknown): string {
  const normalized = normalizeOptionalString(eventName, 128);

  if (!normalized) {
    throw new PluginSupportError(400, 'MISSING_EVENT_NAME', 'Missing event name');
  }

  assertPattern(normalized, EVENT_NAME_PATTERN, 'INVALID_EVENT_NAME', 'event name');
  return normalized;
}

function normalizeDataKey(key: unknown): string {
  const normalized = normalizeOptionalString(key, 128);

  if (!normalized) {
    throw new PluginSupportError(400, 'MISSING_DATA_KEY', 'Missing data key');
  }

  assertPattern(normalized, DATA_KEY_PATTERN, 'INVALID_DATA_KEY', 'data key');
  return normalized;
}

function normalizeDataScope(scope: unknown): string {
  const normalized = normalizeOptionalString(scope, 64) ?? 'server';

  assertPattern(normalized, DATA_SCOPE_PATTERN, 'INVALID_DATA_SCOPE', 'data scope');
  return normalized;
}

function normalizeOwnerId(ownerId: unknown): string {
  return normalizeOptionalString(ownerId, 128) ?? '';
}

function parseJsonValue(valueJson: string): unknown {
  try {
    return JSON.parse(valueJson) as unknown;
  } catch {
    return null;
  }
}

function parseOptionalJsonValue(valueJson: string | null): unknown {
  return valueJson ? parseJsonValue(valueJson) : undefined;
}

function serializeJsonValue(value: unknown, code: string): string {
  try {
    return JSON.stringify(value ?? null);
  } catch {
    throw new PluginSupportError(400, code, 'Value must be JSON serializable');
  }
}

function toRequirementSummary(entity: ServerPluginRequirementEntity): PluginRequirementSummary {
  return {
    installUrl: entity.installUrl ?? undefined,
    manifest: parseOptionalJsonValue(entity.manifestJson),
    pluginId: entity.pluginId,
    reason: entity.reason ?? undefined,
    sourceUrl: entity.sourceUrl ?? undefined,
    status: entity.status,
    updatedAt: entity.updatedAt,
    versionRange: entity.versionRange ?? undefined
  };
}

function toEventDefinitionSummary(entity: ServerPluginEventDefinitionEntity): PluginEventDefinitionSummary {
  return {
    direction: entity.direction,
    eventName: entity.eventName,
    maxPayloadBytes: entity.maxPayloadBytes,
    pluginId: entity.pluginId,
    scope: entity.scope,
    schemaJson: entity.schemaJson ?? undefined,
    updatedAt: entity.updatedAt
  };
}

function toPluginDataRecord(entity: PluginDataEntity): PluginDataRecord {
  return {
    key: entity.key,
    ownerId: entity.ownerId || undefined,
    pluginId: entity.pluginId,
    schemaVersion: entity.schemaVersion,
    scope: entity.scope,
    serverId: entity.serverId,
    updatedAt: entity.updatedAt,
    updatedBy: entity.updatedBy ?? undefined,
    value: parseJsonValue(entity.valueJson)
  };
}

async function assertServerExists(serverId: string) {
  const server = await getServerById(serverId);

  if (!server) {
    throw new PluginSupportError(404, 'SERVER_NOT_FOUND', 'Server not found');
  }

  return server;
}

export async function assertCanManagePluginSupport(serverId: string, actorUserId: string): Promise<void> {
  const server = await assertServerExists(serverId);

  if (!actorUserId || !resolveServerPermission(server, actorUserId, 'manageServer')) {
    throw new PluginSupportError(403, 'NOT_AUTHORIZED', 'Not authorized');
  }
}

export async function assertCanUsePluginData(serverId: string, actorUserId: string): Promise<void> {
  const server = await assertServerExists(serverId);

  if (!actorUserId) {
    throw new PluginSupportError(400, 'MISSING_USER', 'Missing user id');
  }

  if (server.ownerId === actorUserId) {
    return;
  }

  const membership = await findServerMembership(serverId, actorUserId);

  if (!membership) {
    throw new PluginSupportError(403, 'NOT_MEMBER', 'Only joined users can access plugin data');
  }
}

export async function getPluginRequirementsSnapshot(serverId: string): Promise<PluginRequirementsSnapshot> {
  await assertServerExists(serverId);

  const requirementQuery = requirementRepository().find({ where: { serverId } });
  const eventDefinitionQuery = eventDefinitionRepository().find({ where: { serverId } });
  const [requirements, eventDefinitions] = await Promise.all([requirementQuery, eventDefinitionQuery]);
  const requirementSummaries = requirements
    .map(toRequirementSummary)
    .sort((first, second) => first.pluginId.localeCompare(second.pluginId));
  const eventDefinitionSummaries = eventDefinitions
    .map(toEventDefinitionSummary)
    .sort((first, second) => `${first.pluginId}:${first.eventName}`.localeCompare(`${second.pluginId}:${second.eventName}`));
  const updatedAt = Math.max(
    0,
    ...requirementSummaries.map((requirement) => requirement.updatedAt),
    ...eventDefinitionSummaries.map((definition) => definition.updatedAt)
  );

  return {
    eventDefinitions: eventDefinitionSummaries,
    requirements: requirementSummaries,
    serverId,
    updatedAt
  };
}

export async function upsertPluginRequirement(options: {
  actorUserId: string;
  installUrl?: unknown;
  manifest?: unknown;
  pluginId: string;
  reason?: unknown;
  serverId: string;
  sourceUrl?: unknown;
  status: unknown;
  versionRange?: unknown;
}): Promise<PluginRequirementSummary> {
  await assertCanManagePluginSupport(options.serverId, options.actorUserId);

  const pluginId = normalizePluginId(options.pluginId);
  const status = options.status;

  if (!VALID_REQUIREMENT_STATUSES.has(status as ServerPluginRequirementStatus)) {
    throw new PluginSupportError(400, 'INVALID_REQUIREMENT_STATUS', 'Invalid plugin requirement status');
  }

  const repo = requirementRepository();
  const now = Date.now();
  const existing = await repo.findOne({ where: { serverId: options.serverId, pluginId } });
  const entity = repo.create({
    serverId: options.serverId,
    pluginId,
    status: status as ServerPluginRequirementStatus,
    versionRange: normalizeOptionalString(options.versionRange, 128),
    reason: normalizeOptionalString(options.reason, 512),
    installUrl: normalizeOptionalString(options.installUrl, 2_000),
    sourceUrl: normalizeOptionalString(options.sourceUrl, 2_000),
    manifestJson: options.manifest === undefined ? null : serializeJsonValue(options.manifest, 'INVALID_PLUGIN_MANIFEST_METADATA'),
    configuredBy: options.actorUserId,
    createdAt: existing?.createdAt ?? now,
    updatedAt: now
  });

  await repo.save(entity);
  return toRequirementSummary(entity);
}

export async function deletePluginRequirement(options: {
  actorUserId: string;
  pluginId: string;
  serverId: string;
}): Promise<void> {
  await assertCanManagePluginSupport(options.serverId, options.actorUserId);
  await requirementRepository().delete({ serverId: options.serverId, pluginId: normalizePluginId(options.pluginId) });
}

export async function upsertPluginEventDefinition(options: {
  actorUserId: string;
  direction: unknown;
  eventName: string;
  maxPayloadBytes?: unknown;
  pluginId: string;
  rateLimitJson?: unknown;
  schemaJson?: unknown;
  scope: unknown;
  serverId: string;
}): Promise<PluginEventDefinitionSummary> {
  await assertCanManagePluginSupport(options.serverId, options.actorUserId);

  const pluginId = normalizePluginId(options.pluginId);
  const eventName = normalizeEventName(options.eventName);
  const { direction, scope } = options;

  if (!VALID_EVENT_DIRECTIONS.has(direction as ServerPluginEventDirection)) {
    throw new PluginSupportError(400, 'INVALID_EVENT_DIRECTION', 'Invalid plugin event direction');
  }

  if (!VALID_EVENT_SCOPES.has(scope as ServerPluginEventScope)) {
    throw new PluginSupportError(400, 'INVALID_EVENT_SCOPE', 'Invalid plugin event scope');
  }

  const maxPayloadBytes = typeof options.maxPayloadBytes === 'number' && Number.isFinite(options.maxPayloadBytes)
    ? Math.max(1, Math.min(Math.floor(options.maxPayloadBytes), DEFAULT_PLUGIN_EVENT_MAX_PAYLOAD_BYTES))
    : DEFAULT_PLUGIN_EVENT_MAX_PAYLOAD_BYTES;
  const repo = eventDefinitionRepository();
  const now = Date.now();
  const existing = await repo.findOne({ where: { serverId: options.serverId, pluginId, eventName } });
  const entity = repo.create({
    serverId: options.serverId,
    pluginId,
    eventName,
    direction: direction as ServerPluginEventDirection,
    scope: scope as ServerPluginEventScope,
    schemaJson: normalizeOptionalString(options.schemaJson, 10_000),
    maxPayloadBytes,
    rateLimitJson: normalizeOptionalString(options.rateLimitJson, 2_000),
    createdAt: existing?.createdAt ?? now,
    updatedAt: now
  });

  await repo.save(entity);
  return toEventDefinitionSummary(entity);
}

export async function deletePluginEventDefinition(options: {
  actorUserId: string;
  eventName: string;
  pluginId: string;
  serverId: string;
}): Promise<void> {
  await assertCanManagePluginSupport(options.serverId, options.actorUserId);
  await eventDefinitionRepository().delete({
    serverId: options.serverId,
    pluginId: normalizePluginId(options.pluginId),
    eventName: normalizeEventName(options.eventName)
  });
}

export async function listPluginData(options: {
  actorUserId: string;
  key?: unknown;
  ownerId?: unknown;
  pluginId: string;
  scope?: unknown;
  serverId: string;
}): Promise<PluginDataRecord[]> {
  await assertCanUsePluginData(options.serverId, options.actorUserId);

  const pluginId = normalizePluginId(options.pluginId);
  const scope = options.scope === undefined ? undefined : normalizeDataScope(options.scope);
  const ownerId = options.ownerId === undefined ? undefined : normalizeOwnerId(options.ownerId);
  const key = options.key === undefined ? undefined : normalizeDataKey(options.key);
  const query = pluginDataRepository()
    .createQueryBuilder('data')
    .where('data.serverId = :serverId', { serverId: options.serverId })
    .andWhere('data.pluginId = :pluginId', { pluginId });

  if (scope !== undefined) {
    query.andWhere('data.scope = :scope', { scope });
  }

  if (ownerId !== undefined) {
    query.andWhere('data.ownerId = :ownerId', { ownerId });
  }

  if (key !== undefined) {
    query.andWhere('data.key = :key', { key });
  }

  const records = await query
    .orderBy('data.scope', 'ASC')
    .addOrderBy('data.ownerId', 'ASC')
    .addOrderBy('data.key', 'ASC')
    .getMany();

  return records.map(toPluginDataRecord);
}

export async function upsertPluginData(options: {
  actorUserId: string;
  key: string;
  ownerId?: unknown;
  pluginId: string;
  schemaVersion?: unknown;
  scope?: unknown;
  serverId: string;
  value: unknown;
}): Promise<PluginDataRecord> {
  await assertCanUsePluginData(options.serverId, options.actorUserId);

  const pluginId = normalizePluginId(options.pluginId);
  const scope = normalizeDataScope(options.scope);
  const ownerId = scope === 'user' ? normalizeOwnerId(options.ownerId ?? options.actorUserId) : normalizeOwnerId(options.ownerId);

  if (scope === 'user' && ownerId !== options.actorUserId) {
    await assertCanManagePluginSupport(options.serverId, options.actorUserId);
  }

  const key = normalizeDataKey(options.key);
  const schemaVersion = typeof options.schemaVersion === 'number' && Number.isFinite(options.schemaVersion)
    ? Math.max(1, Math.floor(options.schemaVersion))
    : 1;
  const repo = pluginDataRepository();
  const entity = repo.create({
    serverId: options.serverId,
    pluginId,
    scope,
    ownerId,
    key,
    valueJson: serializeJsonValue(options.value, 'INVALID_PLUGIN_DATA'),
    schemaVersion,
    updatedBy: options.actorUserId,
    updatedAt: Date.now()
  });

  await repo.save(entity);
  return toPluginDataRecord(entity);
}

export async function deletePluginData(options: {
  actorUserId: string;
  key: string;
  ownerId?: unknown;
  pluginId: string;
  scope?: unknown;
  serverId: string;
}): Promise<void> {
  await assertCanUsePluginData(options.serverId, options.actorUserId);

  const pluginId = normalizePluginId(options.pluginId);
  const scope = normalizeDataScope(options.scope);
  const ownerId = scope === 'user' ? normalizeOwnerId(options.ownerId ?? options.actorUserId) : normalizeOwnerId(options.ownerId);

  if (scope === 'user' && ownerId !== options.actorUserId) {
    await assertCanManagePluginSupport(options.serverId, options.actorUserId);
  }

  await pluginDataRepository().delete({
    serverId: options.serverId,
    pluginId,
    scope,
    ownerId,
    key: normalizeDataKey(options.key)
  });
}

export async function validatePluginEventEnvelope(envelope: PluginEventEnvelope): Promise<ServerPluginEventDefinitionEntity> {
  const pluginId = normalizePluginId(envelope.pluginId);
  const eventName = normalizeEventName(envelope.eventName);
  const definition = await eventDefinitionRepository().findOne({
    where: {
      serverId: envelope.serverId,
      pluginId,
      eventName
    }
  });

  if (!definition) {
    throw new PluginSupportError(404, 'PLUGIN_EVENT_NOT_REGISTERED', 'Plugin event is not registered for this server');
  }

  if (definition.direction === 'p2pHint') {
    throw new PluginSupportError(400, 'PLUGIN_EVENT_NOT_RELAYABLE', 'P2P plugin events must not be relayed by the signal server');
  }

  const payloadBytes = Buffer.byteLength(serializeJsonValue(envelope.payload, 'INVALID_PLUGIN_EVENT_PAYLOAD'), 'utf8');

  if (payloadBytes > definition.maxPayloadBytes) {
    throw new PluginSupportError(413, 'PLUGIN_EVENT_TOO_LARGE', 'Plugin event payload is too large');
  }

  return definition;
}
