import { createHash } from 'node:crypto';
import { DocumentNode, GraphQLError, parse, print, SourceLocation } from 'graphql';
import { z } from 'zod';
import type { AvailableRulesResponse, PolicyConfigurationObject } from '@hive/policy';
import type { CompositionFailureError } from '@hive/schema';
import type {
  CompositeDeletedSchemaLog,
  CompositePushSchemaLog,
  schema_policy_resource,
  SinglePushSchemaLog,
} from '@hive/storage';
import type {
  AlertChannelType,
  AlertType,
  AuthProviderType,
  OrganizationAccessScope,
  ProjectAccessScope,
  TargetAccessScope,
} from '../__generated__/types';
import type { ResourceAssignmentGroup } from '../modules/organization/lib/resource-assignment-model';
import { parseGraphQLSource, sortDocumentNode } from './schema';

export const NameModel = z
  .string()
  .regex(
    /^([a-z]|[0-9]|\s|\.|,|_|-|\/|&)+$/i,
    `Name restricted to alphanumerical characters, spaces and . , _ - / &`,
  );

export type SingleSchema = SinglePushSchemaLog;
export type DeletedCompositeSchema = CompositeDeletedSchemaLog;
export type PushedCompositeSchema = CompositePushSchemaLog;
export type CompositeSchema = PushedCompositeSchema;

export type Schema = SingleSchema | CompositeSchema;
export type SchemaLog = SingleSchema | PushedCompositeSchema | DeletedCompositeSchema;

export enum SupportTicketPriority {
  NORMAL = 'normal',
  HIGH = 'high',
  URGENT = 'urgent',
}

export enum SupportTicketStatus {
  OPEN = 'open',
  SOLVED = 'solved',
}

export interface DateRange {
  from: Date;
  to: Date;
}

export interface SchemaObject {
  document: DocumentNode;
  source: string;
  url?: string | null;
  raw: string;
}

export const emptySource = '*';

export class GraphQLDocumentStringInvalidError extends Error {
  constructor(message: string, location?: SourceLocation) {
    const locationString = location ? ` at line ${location.line}, column ${location.column}` : '';
    super(`The provided SDL is not valid${locationString}\n: ${message}`);
  }
}

export function hashSDL(sdl: DocumentNode): string {
  const hasher = createHash('md5');
  hasher.update(print(sortDocumentNode(sdl)));
  return hasher.digest('hex');
}

export function createSDLHash(sdl: string): string {
  return hashSDL(
    parse(sdl, {
      noLocation: true,
    }),
  );
}

export type CreateSchemaObjectInput = {
  sdl: string;
  serviceName?: string | null;
  serviceUrl?: string | null;
};

export function createSchemaObject(schema: CreateSchemaObjectInput): SchemaObject {
  let document: DocumentNode;

  try {
    document = parseGraphQLSource(schema.sdl, 'createSchemaObject');
  } catch (err) {
    if (err instanceof GraphQLError) {
      throw new GraphQLDocumentStringInvalidError(err.message, err.locations?.[0]);
    }
    throw err;
  }

  return {
    document,
    raw: schema.sdl,
    source: schema.serviceName ?? emptySource,
    url: schema.serviceUrl ?? null,
  };
}

export enum ProjectType {
  FEDERATION = 'FEDERATION',
  STITCHING = 'STITCHING',
  SINGLE = 'SINGLE',
}

export enum NativeFederationCompatibilityStatusType {
  COMPATIBLE = 'COMPATIBLE',
  INCOMPATIBLE = 'INCOMPATIBLE',
  UNKNOWN = 'UNKNOWN',
  NOT_APPLICABLE = 'NOT_APPLICABLE',
}

export interface OrganizationGetStarted {
  id: string;
  creatingProject: boolean;
  publishingSchema: boolean;
  checkingSchema: boolean;
  invitingMembers: boolean;
  reportingOperations: boolean;
  enablingUsageBasedBreakingChanges: boolean;
}

export interface Organization {
  id: string;
  slug: string;
  name: string;
  billingPlan: string;
  monthlyRateLimit: {
    retentionInDays: number;
    operations: number;
  };
  getStarted: OrganizationGetStarted;
  featureFlags: {
    /**
     * Forces selected targets to use @apollo/federation library
     * when native composition is enabled for a project.
     * This is a temporary solution, requested by one of Hive users.
     *
     * Before enabling native composition on a project, set a feature flag with a list of ids of all targets.
     * We do it this way to allow new targets to use native composition by default and gradually migrate existing ones.
     * The other way around would mean that we would have additional complexity and hard time moving away from this feature flag.
     *
     * @deprecated This feature flag should be removed once no longer needed.
     */
    forceLegacyCompositionInTargets: string[];
    appDeployments: boolean;
    otelTracing: boolean;
    schemaProposals: boolean;
  };
  zendeskId: string | null;
  /** ID of the user that owns the organization */
  ownerId: string;
}

export interface OrganizationInvitation {
  id: string;
  organizationId: string;
  code: string;
  email: string;
  createdAt: string;
  expiresAt: string;
  roleId: string;
  assignedResources: ResourceAssignmentGroup | null;
}

export interface OrganizationBilling {
  organizationId: string;
  externalBillingReference: string;
  billingEmailAddress?: string | null;
}

export interface OIDCIntegration {
  id: string;
  linkedOrganizationId: string;
  clientId: string;
  encryptedClientSecret: string;
  tokenEndpoint: string;
  userinfoEndpoint: string;
  authorizationEndpoint: string;
  additionalScopes: string[];
  oidcUserJoinOnly: boolean;
  oidcUserAccessOnly: boolean;
  requireInvitation: boolean;
  defaultMemberRoleId: string | null;
  defaultResourceAssignment: ResourceAssignmentGroup | null;
}

export interface CDNAccessToken {
  readonly id: string;
  readonly targetId: string;
  readonly s3Key: string;
  readonly firstCharacters: string;
  readonly lastCharacters: string;
  readonly alias: string;
  readonly createdAt: string;
}

export interface DocumentCollection {
  id: string;
  title: string;
  description: string | null;
  targetId: string;
  createdByUserId: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface PreflightScript {
  id: string;
  sourceCode: string;
  targetId: string;
  createdByUserId: string | null;
  createdAt: string;
  updatedAt: string;
}

export type PaginatedDocumentCollections = Readonly<{
  edges: ReadonlyArray<{
    node: DocumentCollection;
    cursor: string;
  }>;
  pageInfo: Readonly<{
    hasNextPage: boolean;
    hasPreviousPage: boolean;
    startCursor: string;
    endCursor: string;
  }>;
}>;

export interface DocumentCollectionOperation {
  id: string;
  title: string;
  contents: string;
  variables: string | null;
  headers: string | null;
  createdByUserId: string | null;
  documentCollectionId: string;
  createdAt: string;
  updatedAt: string;
}

export type PaginatedDocumentCollectionOperations = Readonly<{
  edges: ReadonlyArray<{
    node: DocumentCollectionOperation;
    cursor: string;
  }>;
  pageInfo: Readonly<{
    hasNextPage: boolean;
    hasPreviousPage: boolean;
    startCursor: string;
    endCursor: string;
  }>;
}>;

export type SavedFilterVisibility = 'private' | 'shared';

export interface InsightsFilterData {
  operationHashes: string[];
  clientFilters: Array<{ name: string; versions: string[] | null }>;
  dateRange: { from: string; to: string } | null;
  excludeOperations?: boolean;
  excludeClientFilters?: boolean;
}

export interface SavedFilter {
  id: string;
  projectId: string;
  createdByUserId: string;
  updatedByUserId: string | null;
  name: string;
  description: string | null;
  filters: InsightsFilterData;
  visibility: SavedFilterVisibility;
  viewsCount: number;
  createdAt: string;
  updatedAt: string;
}

export type PaginatedSavedFilters = Readonly<{
  edges: ReadonlyArray<{
    node: SavedFilter;
    cursor: string;
  }>;
  pageInfo: Readonly<{
    hasNextPage: boolean;
    hasPreviousPage: boolean;
    startCursor: string;
    endCursor: string;
  }>;
}>;

export interface Project {
  id: string;
  slug: string;
  orgId: string;
  name: string;
  type: ProjectType;
  createdAt: string;
  buildUrl?: string | null;
  validationUrl?: string | null;
  /**
   * @deprecated A project is no longer linked to a single git repository as a project can be composed of multiple git repositories.
   * TODO: All code referencing this field should be removed at some point.
   */
  gitRepository?: `${string}/${string}` | null;
  useProjectNameInGithubCheck: boolean;
  externalComposition: {
    enabled: boolean;
    endpoint?: string | null;
    encryptedSecret?: string | null;
  };
  nativeFederation: boolean;
}

export interface Target {
  id: string;
  slug: string;
  projectId: string;
  orgId: string;
  name: string;
  graphqlEndpointUrl: string | null;
  failDiffOnDangerousChange: boolean;
}

export interface Token {
  token: string;
  tokenAlias: string;
  name: string;
  target: string;
  project: string;
  organization: string;
  date: string;
  lastUsedAt: string | null;
  scopes: readonly string[];
}

export interface User {
  id: string;
  email: string;
  fullName: string;
  displayName: string;
  providers: AuthProviderType[];
  superTokensUserId: string | null;
  isAdmin: boolean;
  zendeskId: string | null;
}

export interface Member {
  id: string;
  isOwner: boolean;
  user: User;
  organization: string;
  scopes: Array<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
  role: {
    id: string;
    name: string;
    locked: boolean;
    description: string;
    scopes: Array<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
    organizationId: string;
    membersCount: number | undefined;
  };
  oidcIntegrationId: string | null;
  connectedToZendesk: boolean;
}

export interface TargetSettings {
  validation: {
    isEnabled: boolean;
    period: number;
    percentage: number;
    requestCount: number;
    breakingChangeFormula: 'PERCENTAGE' | 'REQUEST_COUNT';
    targets: string[];
    excludedClients: string[];
    excludedAppDeployments: string[];
  };
  failDiffOnDangerousChange: boolean;
  appDeploymentProtection: {
    isEnabled: boolean;
    minDaysInactive: number;
    minDaysSinceCreation: number;
    maxTrafficPercentage: number;
    trafficPeriodDays: number;
    ruleLogic: 'AND' | 'OR';
  };
}

export interface ComposeAndValidateResult {
  supergraph: string | null;
  errors: CompositionFailureError[];
  sdl: string | null;
  contracts: Array<{
    id: string;
    errors: Array<CompositionFailureError>;
    sdl: string | null;
    supergraph: string | null;
  }> | null;
  tags: Array<string> | null;
  schemaMetadata: Record<
    string,
    Array<{ name: string; content: string; source: string | null }>
  > | null;
  metadataAttributes: null | Record<string, string[]>;
  includesNetworkError?: boolean;
  includesException?: boolean;
}

export interface AlertChannel {
  id: string;
  projectId: string;
  type: AlertChannelType;
  name: string;
  createdAt: string;
  slackChannel: string | null;
  webhookEndpoint: string | null;
}

export interface Alert {
  id: string;
  type: AlertType;
  channelId: string;
  organizationId: string;
  projectId: string;
  targetId: string;
  createdAt: string;
}

export interface AdminOrganizationStats {
  organization: Organization;
  versions: number;
  users: number;
  projects: number;
  targets: number;
  persistedOperations: number;
  period: {
    from: Date;
    to: Date;
  };
}

export type SchemaPolicy = {
  id: string;
  createdAt: Date;
  updatedAt: Date;
  config: PolicyConfigurationObject;
  resource: schema_policy_resource;
  resourceId: string;
  allowOverrides: boolean;
};

export type SchemaPolicyAvailableRuleObject = AvailableRulesResponse[0];
