import type { TSchema } from "@sinclair/typebox";

import { Type } from "@sinclair/typebox";

import type { CheckerDefinition } from "../runner/types";

import {
  createAuthoringContentExpectationsSchema,
  createAuthoringFieldSchema,
  createAuthoringKeyedExpectationsSchema,
  createAuthoringValueExpectationSchema,
  createValueMatcherObjectSchema,
  durationSchema,
  sizeSchema,
  variableValueSchema,
} from "./fragments";

const LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "fatal"] as const;
const ROTATION_FREQUENCIES = ["hourly", "daily", "weekly"] as const;
type SchemaKind = "authoring" | "normalized";

export function createAuthoringProbeConfigSchema(checkers: CheckerDefinition[], external = false): TSchema {
  return createProbeConfigSchemaForKind(checkers, "authoring", external);
}

export function createAuthoringTargetSchema(checker: CheckerDefinition): TSchema {
  return createTargetSchemaForKind(checker, "authoring");
}

export function createExternalProbeConfigSchema(checkers: CheckerDefinition[]): Record<string, unknown> {
  return {
    ...cloneSchema(createAuthoringProbeConfigSchema(checkers, true)),
    $id: "https://dial.local/probe-config.schema.json",
    $schema: "http://json-schema.org/draft-07/schema#",
    definitions: {
      ContentExpectations: cloneSchema(createAuthoringContentExpectationsSchema()),
      KeyedExpectations: cloneSchema(createAuthoringKeyedExpectationsSchema()),
      ValueExpectation: cloneSchema(createAuthoringValueExpectationSchema()),
      ValueMatcher: cloneSchema(createValueMatcherObjectSchema()),
    },
  };
}

export function createNormalizedProbeConfigSchema(checkers: CheckerDefinition[], external = false): TSchema {
  return createProbeConfigSchemaForKind(checkers, "normalized", external);
}

export function createNormalizedTargetSchema(checker: CheckerDefinition): TSchema {
  return createTargetSchemaForKind(checker, "normalized");
}

export function createProbeConfigSchema(checkers: CheckerDefinition[], external = false): TSchema {
  return createNormalizedProbeConfigSchema(checkers, external);
}

export function createTargetSchema(checker: CheckerDefinition): TSchema {
  return createNormalizedTargetSchema(checker);
}

function cloneSchema(schema: TSchema): Record<string, unknown> {
  return JSON.parse(JSON.stringify(schema)) as Record<string, unknown>;
}

function createBaseTargetSchema(checkers: CheckerDefinition[], kind: SchemaKind): TSchema {
  return Type.Object(
    {
      description: Type.Optional(Type.Union([Type.Null(), stringForKind(kind, { maxLength: 500 })])),
      group: Type.Optional(Type.String()),
      id: Type.String({ maxLength: 30, minLength: 1 }),
      interval: Type.Optional(durationSchema),
      name: Type.Optional(Type.Union([Type.Null(), stringForKind(kind, { maxLength: 30, minLength: 1 })])),
      timeout: Type.Optional(durationSchema),
      type: Type.Union(checkers.map((checker) => Type.Literal(checker.type)) as unknown as [TSchema, ...TSchema[]]),
    },
    { additionalProperties: true },
  );
}

function createExternalTargetSchema(checkers: CheckerDefinition[], kind: SchemaKind): TSchema {
  return Type.Union(checkers.map((checker) => createTargetSchemaForKind(checker, kind)) as [TSchema, ...TSchema[]]);
}

function createLoggingSchema(kind: SchemaKind): TSchema {
  const logLevelSchema = Type.Union(LOG_LEVELS.map((l) => Type.Literal(l)) as unknown as [TSchema, ...TSchema[]]);
  const logLevel = enumForKind(kind, logLevelSchema);
  const frequency = enumForKind(
    kind,
    Type.Union(ROTATION_FREQUENCIES.map((f) => Type.Literal(f)) as unknown as [TSchema, ...TSchema[]]),
  );
  return Type.Object(
    {
      console: Type.Optional(Type.Object({ level: Type.Optional(logLevel) }, { additionalProperties: false })),
      file: Type.Optional(
        Type.Object(
          {
            level: Type.Optional(logLevel),
            path: Type.Optional(Type.String({ minLength: 1 })),
            rotation: Type.Optional(
              Type.Object(
                {
                  frequency: Type.Optional(frequency),
                  maxFiles: Type.Optional(integerForKind(kind, { minimum: 1 })),
                  size: Type.Optional(sizeSchema),
                },
                { additionalProperties: false },
              ),
            ),
          },
          { additionalProperties: false },
        ),
      ),
      level: Type.Optional(logLevel),
    },
    { additionalProperties: false },
  );
}

function createProbeConfigSchemaForKind(checkers: CheckerDefinition[], kind: SchemaKind, external: boolean): TSchema {
  const properties: Record<string, TSchema> = {
    probes: Type.Optional(createProbesSchema(kind)),
    server: Type.Optional(createServerSchema(kind)),
    targets: Type.Array(
      external ? createExternalTargetSchema(checkers, kind) : createBaseTargetSchema(checkers, kind),
      {
        minItems: 1,
      },
    ),
  };
  if (kind === "authoring") {
    properties["variables"] = Type.Optional(
      Type.Record(Type.String({ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$" }), variableValueSchema),
    );
  }
  return Type.Object(properties, { additionalProperties: false });
}

function createProbesSchema(kind: SchemaKind): TSchema {
  return Type.Object(
    {
      execution: Type.Optional(
        Type.Object(
          {
            maxConcurrentChecks: Type.Optional(integerForKind(kind, { minimum: 1 })),
          },
          { additionalProperties: false },
        ),
      ),
    },
    { additionalProperties: false },
  );
}

function createServerSchema(kind: SchemaKind): TSchema {
  return Type.Object(
    {
      listen: Type.Optional(
        Type.Object(
          {
            host: Type.Optional(Type.String()),
            port: Type.Optional(integerForKind(kind, { maximum: 65535, minimum: 0 })),
          },
          { additionalProperties: false },
        ),
      ),
      logging: Type.Optional(createLoggingSchema(kind)),
      storage: Type.Optional(
        Type.Object(
          {
            dataDir: Type.Optional(Type.String()),
            retention: Type.Optional(durationSchema),
          },
          { additionalProperties: false },
        ),
      ),
    },
    { additionalProperties: false },
  );
}

function createTargetSchemaForKind(checker: CheckerDefinition, kind: SchemaKind): TSchema {
  const properties: Record<string, TSchema> = {
    description: Type.Optional(Type.Union([Type.Null(), stringForKind(kind, { maxLength: 500 })])),
    expect: Type.Optional(checker.schemas[kind].expect),
    group: Type.Optional(Type.String()),
    id: Type.String({ maxLength: 30, minLength: 1 }),
    interval: Type.Optional(durationSchema),
    name: Type.Optional(Type.Union([Type.Null(), stringForKind(kind, { maxLength: 30, minLength: 1 })])),
    timeout: Type.Optional(durationSchema),
    type: Type.Literal(checker.type),
  };
  properties[checker.configKey] = checker.schemas[kind].config;
  return Type.Object(properties, { additionalProperties: false });
}

function enumForKind(kind: SchemaKind, schema: TSchema): TSchema {
  return kind === "authoring" ? createAuthoringFieldSchema(schema) : schema;
}

function integerForKind(kind: SchemaKind, options?: Parameters<typeof Type.Integer>[0]): TSchema {
  const schema = Type.Integer(options);
  return kind === "authoring" ? createAuthoringFieldSchema(schema) : schema;
}

function stringForKind(kind: SchemaKind, options?: Parameters<typeof Type.String>[0]): TSchema {
  const schema = Type.String(options);
  return kind === "authoring" ? createAuthoringFieldSchema(schema) : schema;
}
