import {
  PLUGIN_CAPABILITIES,
  PLUGIN_EVENT_DIRECTIONS,
  PLUGIN_EVENT_SCOPES,
  type PluginCapabilityId,
  type TojuPluginManifest
} from '../../../../shared-kernel';
import type { PluginManifestValidationResult, PluginValidationIssue } from '../models/plugin-runtime.models';

const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{1,126}[a-z0-9]$/;
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
const capabilitySet = new Set<string>(PLUGIN_CAPABILITIES);
const eventDirectionSet = new Set<string>(PLUGIN_EVENT_DIRECTIONS);
const eventScopeSet = new Set<string>(PLUGIN_EVENT_SCOPES);

function isRecord(value: unknown): value is Record<string, unknown> {
  return !!value && typeof value === 'object' && !Array.isArray(value);
}

function readString(record: Record<string, unknown>, key: string): string | null {
  const value = record[key];

  return typeof value === 'string' ? value.trim() : null;
}

function pushIssue(
  issues: PluginValidationIssue[],
  path: string,
  message: string,
  severity: PluginValidationIssue['severity'] = 'error'
): void {
  issues.push({ path, message, severity });
}

function validateStringField(
  issues: PluginValidationIssue[],
  record: Record<string, unknown>,
  key: string,
  options?: { pattern?: RegExp; required?: boolean }
): void {
  const value = readString(record, key);

  if (!value) {
    if (options?.required !== false) {
      pushIssue(issues, key, `${key} is required`);
    }

    return;
  }

  if (options?.pattern && !options.pattern.test(value)) {
    pushIssue(issues, key, `${key} has an invalid format`);
  }
}

function validateStringArray(
  issues: PluginValidationIssue[],
  value: unknown,
  path: string
): void {
  if (value === undefined) {
    return;
  }

  if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || !entry.trim())) {
    pushIssue(issues, path, `${path} must be an array of non-empty strings`);
  }
}

function validateRelationships(issues: PluginValidationIssue[], manifestRecord: Record<string, unknown>): void {
  const relationships = manifestRecord['relationships'];

  if (relationships === undefined) {
    return;
  }

  if (!isRecord(relationships)) {
    pushIssue(issues, 'relationships', 'relationships must be an object');
    return;
  }

  validateStringArray(issues, relationships['after'], 'relationships.after');
  validateStringArray(issues, relationships['before'], 'relationships.before');
  validateStringArray(issues, relationships['conflicts'], 'relationships.conflicts');

  for (const key of ['requires', 'optional'] as const) {
    const entries = relationships[key];

    if (entries === undefined) {
      continue;
    }

    if (!Array.isArray(entries)) {
      pushIssue(issues, `relationships.${key}`, `relationships.${key} must be an array`);
      continue;
    }

    entries.forEach((entry, index) => {
      if (!isRecord(entry) || typeof entry['id'] !== 'string' || !entry['id'].trim()) {
        pushIssue(issues, `relationships.${key}.${index}`, 'dependency id is required');
      }
    });
  }
}

function validateCapabilities(issues: PluginValidationIssue[], manifestRecord: Record<string, unknown>): void {
  const capabilities = manifestRecord['capabilities'];

  if (capabilities === undefined) {
    return;
  }

  if (!Array.isArray(capabilities)) {
    pushIssue(issues, 'capabilities', 'capabilities must be an array');
    return;
  }

  capabilities.forEach((capability, index) => {
    if (typeof capability !== 'string' || !capabilitySet.has(capability)) {
      pushIssue(issues, `capabilities.${index}`, `Unknown capability ${String(capability)}`);
    }
  });
}

function validateEvents(issues: PluginValidationIssue[], manifestRecord: Record<string, unknown>): void {
  const events = manifestRecord['events'];

  if (events === undefined) {
    return;
  }

  if (!Array.isArray(events)) {
    pushIssue(issues, 'events', 'events must be an array');
    return;
  }

  events.forEach((event, index) => {
    if (!isRecord(event)) {
      pushIssue(issues, `events.${index}`, 'event must be an object');
      return;
    }

    if (typeof event['eventName'] !== 'string' || !event['eventName'].trim()) {
      pushIssue(issues, `events.${index}.eventName`, 'eventName is required');
    }

    if (typeof event['direction'] !== 'string' || !eventDirectionSet.has(event['direction'])) {
      pushIssue(issues, `events.${index}.direction`, 'direction is invalid');
    }

    if (typeof event['scope'] !== 'string' || !eventScopeSet.has(event['scope'])) {
      pushIssue(issues, `events.${index}.scope`, 'scope is invalid');
    }
  });
}

export function validateTojuPluginManifest(value: unknown): PluginManifestValidationResult {
  const issues: PluginValidationIssue[] = [];

  if (!isRecord(value)) {
    return {
      issues: [{ path: '', message: 'Manifest must be an object', severity: 'error' }],
      valid: false
    };
  }

  validateStringField(issues, value, 'id', { pattern: PLUGIN_ID_PATTERN });
  validateStringField(issues, value, 'title');
  validateStringField(issues, value, 'description');
  validateStringField(issues, value, 'version', { pattern: VERSION_PATTERN });
  validateStringField(issues, value, 'apiVersion');

  if (value['schemaVersion'] !== 1) {
    pushIssue(issues, 'schemaVersion', 'schemaVersion must be 1');
  }

  if (value['kind'] !== 'client' && value['kind'] !== 'library') {
    pushIssue(issues, 'kind', 'kind must be client or library');
  }

  if (value['scope'] !== undefined && value['scope'] !== 'client' && value['scope'] !== 'server') {
    pushIssue(issues, 'scope', 'scope must be client or server');
  }

  if (!isRecord(value['compatibility'])) {
    pushIssue(issues, 'compatibility', 'compatibility is required');
  } else {
    validateStringField(issues, value['compatibility'], 'minimumTojuVersion');
  }

  if (typeof value['entrypoint'] !== 'string' && value['kind'] === 'client') {
    pushIssue(issues, 'entrypoint', 'client plugins require an entrypoint');
  }

  validateCapabilities(issues, value);
  validateRelationships(issues, value);
  validateEvents(issues, value);

  return {
    issues,
    manifest: issues.some((issue) => issue.severity === 'error') ? undefined : value as unknown as TojuPluginManifest,
    valid: !issues.some((issue) => issue.severity === 'error')
  };
}

export function isKnownPluginCapability(value: string): value is PluginCapabilityId {
  return capabilitySet.has(value);
}
