import type { StandardSchemaV1 } from "@standard-schema/spec";
import {
  createProviderInstrumentation,
  type ProviderInstrumentation,
  type ProviderInstrumentationTarget,
} from "../providers/index.js";
import { runWithResolvedTracingContext } from "../tracing/execution.js";
import type { TracingPort } from "../tracing/index.js";

/**
 * Any Standard Schema compatible validator.
 */
export type StandardSchema = StandardSchemaV1<unknown, unknown>;

/**
 * Value or promise of that value.
 */
export type MaybePromise<T> = T | Promise<T>;

/**
 * Infer the parsed output type from a Standard Schema.
 */
export type InferSchemaOutput<T extends StandardSchemaV1> =
  StandardSchemaV1.InferOutput<T>;

/**
 * Date input accepted by schedule runners.
 */
export type ScheduleDateInput = Date | string | number;

/**
 * Metadata for one schedule run.
 */
export interface ScheduleRunContext {
  /**
   * Optional provider run ID.
   */
  readonly id?: string;
  /**
   * One-based provider attempt number for this run, when available.
   */
  readonly attempt?: number;
  /**
   * Time the provider planned the run.
   */
  readonly scheduledAt?: Date;
  /**
   * Time the runner triggered this execution.
   */
  readonly triggeredAt: Date;
  /**
   * Optional provider or app source label.
   */
  readonly source?: string;
}

/**
 * Minimal schedule definition shape accepted by schedule helpers.
 */
export interface SchedulePayloadDef<
  Name extends string = string,
  Payload extends StandardSchema = StandardSchema,
> {
  /**
   * Stable schedule name.
   */
  readonly name: Name;
  /**
   * Standard Schema payload validator.
   */
  readonly payload: Payload;
}

/**
 * Schedule definition created by `defineSchedule(...)`.
 */
export interface ScheduleDef<
  Name extends string = string,
  Payload extends StandardSchema = StandardSchema,
  Ctx = unknown,
> extends SchedulePayloadDef<Name, Payload> {
  /**
   * Discriminator for schedule definitions.
   */
  readonly kind: "schedule";
  /**
   * Cron expression consumed by schedule providers.
   */
  readonly cron: string;
  /**
   * Optional IANA timezone consumed by schedule providers.
   */
  readonly timezone?: string;
  /**
   * Optional human-readable description for docs and tooling.
   */
  readonly description?: string;
  /**
   * Build a payload when the provider does not supply one.
   */
  createPayload?(
    args: ScheduleCreatePayloadArgs<ScheduleDef<Name, Payload, Ctx>>,
  ): MaybePromise<InferSchemaOutput<Payload>>;
  /**
   * Handle a parsed schedule payload.
   */
  handle(
    args: ScheduleHandleArgs<ScheduleDef<Name, Payload, Ctx>, Ctx>,
  ): MaybePromise<void>;
}

/**
 * Infer the parsed payload type for a schedule definition.
 */
export type InferSchedulePayload<S extends SchedulePayloadDef> =
  S["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;

/**
 * Arguments passed to a schedule `createPayload` callback.
 */
export interface ScheduleCreatePayloadArgs<S extends SchedulePayloadDef> {
  /**
   * Schedule definition being run.
   */
  schedule: S;
  /**
   * Run metadata.
   */
  run: ScheduleRunContext;
}

/**
 * Arguments passed to a schedule handler.
 */
export interface ScheduleHandleArgs<S extends ScheduleDef, Ctx> {
  /**
   * Schedule definition being handled.
   */
  schedule: S;
  /**
   * Parsed schedule payload.
   */
  payload: InferSchedulePayload<S>;
  /** Handler context. */
  ctx: Ctx;
  /**
   * Run metadata.
   */
  run: ScheduleRunContext;
}

/**
 * Options for `defineSchedule(...)`.
 */
export interface DefineScheduleOptions<
  Name extends string,
  Payload extends StandardSchema,
  Ctx,
> {
  /**
   * Cron expression consumed by schedule providers.
   */
  cron: string;
  /**
   * Optional IANA timezone consumed by schedule providers.
   */
  timezone?: string;
  /**
   * Standard Schema payload validator.
   */
  payload: Payload;
  /**
   * Optional human-readable description for docs and tooling.
   */
  description?: string;
  /**
   * Build a payload when the provider does not supply one.
   */
  createPayload?(
    args: ScheduleCreatePayloadArgs<ScheduleDef<Name, Payload, Ctx>>,
  ): MaybePromise<InferSchemaOutput<Payload>>;
  /**
   * Handle a parsed schedule payload.
   */
  handle(
    args: ScheduleHandleArgs<ScheduleDef<Name, Payload, Ctx>, Ctx>,
  ): MaybePromise<void>;
}

/**
 * Options for one manual schedule run.
 */
export interface ScheduleRunOptions<Payload = unknown> {
  /**
   * Payload supplied by the provider or manual runner.
   */
  payload?: Payload;
  /**
   * Optional provider run ID.
   */
  id?: string;
  /**
   * One-based provider attempt number for this run, when available.
   */
  attempt?: number;
  /**
   * Time the provider planned the run.
   */
  scheduledAt?: ScheduleDateInput;
  /**
   * Time the runner triggered the execution.
   */
  triggeredAt?: ScheduleDateInput;
  /**
   * Optional provider or app source label.
   */
  source?: string;
}

/**
 * Arguments for `runSchedule(...)`.
 */
export type ScheduleRunArgs<
  Ctx,
  Payload = unknown,
> = ScheduleRunOptions<Payload> & {
  /** Handler context or factory resolved inside the schedule span. */
  ctx: Ctx | (() => MaybePromise<Ctx>);
  /** Runtime tracing port used before a lazy context factory runs. */
  tracing?: TracingPort;
};

/**
 * Arguments passed to schedule lifecycle hooks.
 */
export interface ScheduleLifecycleArgs<S extends ScheduleDef = ScheduleDef> {
  /**
   * Schedule definition being run.
   */
  schedule: S;
  /**
   * Parsed schedule payload.
   */
  payload: InferSchedulePayload<S>;
  /**
   * Run metadata.
   */
  run: ScheduleRunContext;
}

/**
 * Arguments passed to a schedule error hook.
 */
export interface ScheduleErrorArgs<S extends ScheduleDef = ScheduleDef> {
  /**
   * Schedule definition being run.
   */
  schedule: S;
  /**
   * Parsed payload when validation or payload creation completed.
   */
  payload?: InferSchedulePayload<S>;
  /**
   * Run metadata.
   */
  run: ScheduleRunContext;
  /**
   * Error thrown by payload creation, validation, or the handler.
   */
  error: unknown;
}

/**
 * Schedule lifecycle hook names.
 */
export type ScheduleHookName = "start" | "success" | "error";

/**
 * Devtools event recorded by the inline schedule runner for each run.
 */
export interface ScheduleDevtoolsEvent {
  /**
   * Devtools event type.
   */
  type: "schedule";
  /**
   * Watcher category used by devtools.
   */
  watcher: "schedules";
  /**
   * Stable schedule name.
   */
  scheduleName: string;
  /**
   * Schedule run lifecycle status.
   */
  status: "started" | "completed" | "failed";
  /**
   * Cron expression for the schedule.
   */
  cron: string;
  /**
   * IANA timezone for the schedule, when declared.
   */
  timezone?: string;
  /**
   * Request correlation ID, when the trigger ran inside a request.
   */
  requestId?: string;
  /**
   * Trace identifier for distributed tracing integrations.
   */
  traceId?: string;
  /**
   * Structured run details such as `source`, `scheduledAt`, and `error`.
   */
  details?: Record<string, unknown>;
}

/**
 * Correlation fields attached to schedule instrumentation events.
 */
export interface ScheduleInstrumentationContext {
  /**
   * Request correlation ID for the triggering invocation.
   */
  requestId?: string;
  /**
   * Trace identifier for the triggering invocation.
   */
  traceId?: string;
}

/**
 * Arguments passed when a schedule lifecycle hook itself fails.
 */
export interface ScheduleHookErrorArgs<S extends ScheduleDef = ScheduleDef> {
  /**
   * Schedule definition being run.
   */
  schedule: S;
  /**
   * Parsed payload when available.
   */
  payload?: InferSchedulePayload<S>;
  /**
   * Run metadata.
   */
  run: ScheduleRunContext;
  /**
   * Lifecycle hook that failed.
   */
  hook: ScheduleHookName;
  /**
   * Hook error.
   */
  error: unknown;
  /**
   * Original schedule error when the failing hook is `onError`.
   */
  scheduleError?: unknown;
}

/**
 * Options for the inline schedule runner.
 */
export interface InlineScheduleRunnerOptions<Ctx> {
  /**
   * Static schedule context or factory evaluated for each run.
   */
  ctx?: Ctx | (() => MaybePromise<Ctx>);
  /** Runtime tracing port used before a lazy context factory runs. */
  tracing?: TracingPort;
  /**
   * Clock used when run timestamps are not provided.
   */
  now?: () => Date;
  /**
   * Provider instrumentation target that receives `schedule` events for each
   * run. Pass `ctx.ports`, `ctx.ports.instrumentation`, or
   * `ctx.ports.devtools` directly.
   *
   * The runner records `started`, `completed`, and `failed` events. Recording
   * failures are isolated from schedule execution.
   */
  instrumentation?: ProviderInstrumentationTarget;
  /**
   * Correlation fields attached to recorded schedule events.
   */
  instrumentationContext?: ScheduleInstrumentationContext;
  /**
   * Called after payload validation and before the schedule handler.
   */
  onStart?<S extends ScheduleDef<string, StandardSchema, Ctx>>(
    args: ScheduleLifecycleArgs<S>,
  ): MaybePromise<void>;
  /**
   * Called after the schedule handler completes.
   */
  onSuccess?<S extends ScheduleDef<string, StandardSchema, Ctx>>(
    args: ScheduleLifecycleArgs<S>,
  ): MaybePromise<void>;
  /**
   * Called when payload creation, validation, or the handler fails.
   */
  onError?<S extends ScheduleDef<string, StandardSchema, Ctx>>(
    args: ScheduleErrorArgs<S>,
  ): MaybePromise<void>;
  /**
   * Called when a lifecycle hook throws.
   */
  onHookError?<S extends ScheduleDef<string, StandardSchema, Ctx>>(
    args: ScheduleHookErrorArgs<S>,
  ): MaybePromise<void>;
}

/**
 * Port shape for running schedules.
 */
export interface ScheduleRunnerPort<Ctx = unknown> {
  /**
   * Run a schedule with optional provider metadata and payload.
   */
  run<S extends ScheduleDef<string, StandardSchema, Ctx>>(
    schedule: S,
    options?: ScheduleRunOptions<InferSchedulePayload<S>>,
  ): Promise<void>;
}

/**
 * Local/test schedule runner that executes handlers inline.
 */
export interface InlineScheduleRunner<Ctx = unknown>
  extends ScheduleRunnerPort<Ctx> {}

/**
 * Context-bound schedule helper factory.
 */
export interface Schedules<Ctx> {
  /**
   * Define a schedule with the bound context type.
   */
  defineSchedule<Name extends string, Payload extends StandardSchema>(
    name: Name,
    options: DefineScheduleOptions<Name, Payload, Ctx>,
  ): ScheduleDef<Name, Payload, Ctx>;
}

/**
 * Error thrown when schedule payload validation fails.
 */
export class ScheduleValidationError extends Error {
  /**
   * Raw Standard Schema validation issues.
   */
  readonly issues: readonly StandardSchemaV1.Issue[];

  constructor(args: {
    name: string;
    issues: readonly StandardSchemaV1.Issue[];
  }) {
    super(
      `Schedule "${args.name}" payload validation failed: ${formatIssues(args.issues)}`,
    );
    this.name = "ScheduleValidationError";
    this.issues = args.issues;
  }
}

/**
 * Error thrown when schedule run metadata cannot be normalized.
 */
export class ScheduleRunContextError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ScheduleRunContextError";
  }
}

function formatPath(path: StandardSchemaV1.Issue["path"]): string {
  if (!path?.length) return "";

  return path
    .map((segment) =>
      typeof segment === "object" && segment !== null && "key" in segment
        ? String(segment.key)
        : String(segment),
    )
    .join(".");
}

function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
  return issues
    .map((issue) => {
      const path = formatPath(issue.path);
      return path ? `${path}: ${issue.message}` : issue.message;
    })
    .join("; ");
}

async function parsePayload<Schema extends StandardSchemaV1>(
  schema: Schema,
  input: unknown,
  args: { name: string },
): Promise<InferSchemaOutput<Schema>> {
  const result = await schema["~standard"].validate(input);

  if (result.issues?.length) {
    throw new ScheduleValidationError({
      name: args.name,
      issues: result.issues,
    });
  }

  if ("value" in result) {
    return result.value as InferSchemaOutput<Schema>;
  }

  throw new Error("Invalid Standard Schema result: missing value");
}

function normalizeDate(
  value: ScheduleDateInput | undefined,
  field: string,
  fallback: () => Date,
): Date {
  if (value === undefined) return fallback();

  const date =
    value instanceof Date ? new Date(value.getTime()) : new Date(value);

  if (Number.isNaN(date.getTime())) {
    throw new ScheduleRunContextError(
      `Schedule run ${field} must be a valid date.`,
    );
  }

  return date;
}

function normalizeOptionalDate(
  value: ScheduleDateInput | undefined,
  field: string,
): Date | undefined {
  if (value === undefined) return undefined;
  return normalizeDate(value, field, () => new Date());
}

function normalizeOptionalAttempt(
  value: number | undefined,
): number | undefined {
  if (value === undefined) return undefined;

  if (!Number.isInteger(value) || value < 1) {
    throw new ScheduleRunContextError(
      "Schedule run attempt must be a positive integer.",
    );
  }

  return value;
}

function createRunContext(
  options: ScheduleRunOptions<unknown>,
  now: () => Date,
): ScheduleRunContext {
  return {
    id: options.id,
    attempt: normalizeOptionalAttempt(options.attempt),
    scheduledAt: normalizeOptionalDate(options.scheduledAt, "scheduledAt"),
    triggeredAt: normalizeDate(options.triggeredAt, "triggeredAt", now),
    source: options.source,
  };
}

function scheduleTraceOperation(
  schedule: ScheduleDef,
  options: ScheduleRunOptions<unknown>,
) {
  const attributes = {
    "beignet.schedule.name": schedule.name,
    ...(options.attempt === undefined
      ? {}
      : { "beignet.schedule.attempt": options.attempt }),
    ...(options.source === undefined
      ? {}
      : { "beignet.schedule.source": options.source }),
  } as const;
  const metricAttributes = {
    "beignet.schedule.name": schedule.name,
    ...(options.attempt === undefined
      ? {}
      : { "beignet.schedule.attempt": options.attempt }),
  } as const;

  return {
    name: `beignet.schedule ${schedule.name}`,
    type: "schedule" as const,
    kind: "consumer" as const,
    attributes,
    metricAttributes,
  };
}

async function resolveSchedulePayload<S extends ScheduleDef>(
  schedule: S,
  options: ScheduleRunOptions<InferSchedulePayload<S>>,
  run: ScheduleRunContext,
): Promise<InferSchedulePayload<S>> {
  const hasExplicitPayload = Object.hasOwn(options, "payload");
  const rawPayload = hasExplicitPayload
    ? options.payload
    : await schedule.createPayload?.({ schedule, run });

  return parseSchedulePayload(schedule, rawPayload);
}

async function reportHookError<
  Ctx,
  S extends ScheduleDef<string, StandardSchema, Ctx>,
>(
  onHookError: InlineScheduleRunnerOptions<Ctx>["onHookError"] | undefined,
  args: ScheduleHookErrorArgs<S>,
): Promise<void> {
  try {
    await onHookError?.(args);
  } catch {
    // Hook failures are isolated from schedule execution.
  }
}

async function runLifecycleHook<
  Ctx,
  S extends ScheduleDef<string, StandardSchema, Ctx>,
>(
  hook: Exclude<ScheduleHookName, "error">,
  handler: ((args: ScheduleLifecycleArgs<S>) => MaybePromise<void>) | undefined,
  onHookError: InlineScheduleRunnerOptions<Ctx>["onHookError"] | undefined,
  args: ScheduleLifecycleArgs<S>,
): Promise<void> {
  try {
    await handler?.(args);
  } catch (error) {
    await reportHookError(onHookError, { ...args, hook, error });
  }
}

async function runErrorHook<
  Ctx,
  S extends ScheduleDef<string, StandardSchema, Ctx>,
>(
  handler: ((args: ScheduleErrorArgs<S>) => MaybePromise<void>) | undefined,
  onHookError: InlineScheduleRunnerOptions<Ctx>["onHookError"] | undefined,
  args: ScheduleErrorArgs<S>,
): Promise<void> {
  try {
    await handler?.(args);
  } catch (error) {
    await reportHookError(onHookError, {
      schedule: args.schedule,
      payload: args.payload,
      run: args.run,
      hook: "error",
      error,
      scheduleError: args.error,
    });
  }
}

async function recordScheduleEvent<
  Ctx,
  S extends ScheduleDef<string, StandardSchema, Ctx>,
>(
  instrumentation: ProviderInstrumentation,
  instrumentationContext: ScheduleInstrumentationContext | undefined,
  schedule: S,
  status: ScheduleDevtoolsEvent["status"],
  run: ScheduleRunContext,
  details?: Record<string, unknown>,
): Promise<void> {
  instrumentation.record({
    type: "schedule",
    watcher: "schedules",
    requestId: instrumentationContext?.requestId,
    traceId: instrumentationContext?.traceId,
    scheduleName: schedule.name,
    status,
    cron: schedule.cron,
    timezone: schedule.timezone,
    details: {
      source: run.source,
      scheduledAt: run.scheduledAt?.toISOString(),
      ...details,
    },
  });
}

function defineScheduleImpl<
  Name extends string,
  Payload extends StandardSchema,
  Ctx = unknown,
>(
  name: Name,
  options: DefineScheduleOptions<Name, Payload, Ctx>,
): ScheduleDef<Name, Payload, Ctx> {
  return {
    kind: "schedule",
    name,
    cron: options.cron,
    timezone: options.timezone,
    payload: options.payload,
    description: options.description,
    createPayload: options.createPayload as
      | ScheduleDef<Name, Payload, Ctx>["createPayload"]
      | undefined,
    handle: options.handle as ScheduleDef<Name, Payload, Ctx>["handle"],
  };
}

/**
 * Validate and parse a schedule payload with the schedule's Standard Schema.
 */
export async function parseSchedulePayload<S extends SchedulePayloadDef>(
  schedule: S,
  payload: unknown,
): Promise<InferSchedulePayload<S>> {
  return (await parsePayload(schedule.payload, payload, {
    name: schedule.name,
  })) as InferSchedulePayload<S>;
}

/**
 * Run one schedule directly with an explicit context.
 */
export async function runSchedule<
  Ctx,
  S extends ScheduleDef<string, StandardSchema, Ctx>,
>(
  schedule: S,
  args: ScheduleRunArgs<Ctx, InferSchedulePayload<S>>,
): Promise<void> {
  await runWithResolvedTracingContext({
    tracing: args.tracing,
    ctx: args.ctx,
    operation: scheduleTraceOperation(schedule, args),
    run: async (ctx) => {
      const run = createRunContext(args, () => new Date());
      const payload = await resolveSchedulePayload(schedule, args, run);

      await schedule.handle({
        schedule,
        payload,
        ctx,
        run,
      });
    },
  });
}

/**
 * Create a local/test schedule runner that executes handlers inline.
 */
export function createInlineScheduleRunner<Ctx>(
  options: InlineScheduleRunnerOptions<Ctx> = {},
): InlineScheduleRunner<Ctx> {
  const now = options.now ?? (() => new Date());
  const instrumentation = createProviderInstrumentation(
    options.instrumentation,
    {
      providerName: "schedules",
      watcher: "schedules",
    },
  );

  return {
    async run<S extends ScheduleDef<string, StandardSchema, Ctx>>(
      schedule: S,
      runOptions: ScheduleRunOptions<InferSchedulePayload<S>> = {},
    ) {
      const run = createRunContext(runOptions, now);
      let payload: InferSchedulePayload<S> | undefined;

      try {
        payload = await resolveSchedulePayload(schedule, runOptions, run);

        const lifecycleArgs = { schedule, payload, run };
        await recordScheduleEvent(
          instrumentation,
          options.instrumentationContext,
          schedule,
          "started",
          run,
        );
        await runLifecycleHook(
          "start",
          options.onStart,
          options.onHookError,
          lifecycleArgs,
        );
        await runWithResolvedTracingContext({
          tracing: options.tracing,
          ctx: options.ctx as Ctx | (() => MaybePromise<Ctx>),
          operation: scheduleTraceOperation(schedule, runOptions),
          run: (ctx) =>
            schedule.handle({
              schedule,
              payload: payload as InferSchedulePayload<S>,
              ctx,
              run,
            }),
        });
        await recordScheduleEvent(
          instrumentation,
          options.instrumentationContext,
          schedule,
          "completed",
          run,
        );
        await runLifecycleHook(
          "success",
          options.onSuccess,
          options.onHookError,
          lifecycleArgs,
        );
      } catch (error) {
        await recordScheduleEvent(
          instrumentation,
          options.instrumentationContext,
          schedule,
          "failed",
          run,
          { error },
        );
        await runErrorHook(options.onError, options.onHookError, {
          error,
          schedule,
          payload,
          run,
        });
        throw error;
      }
    },
  };
}

/**
 * Create schedule helper methods bound to an application context type.
 *
 * Call it once in `lib/schedules.ts`:
 *
 * ```ts
 * export const { defineSchedule } = createSchedules<AppContext>();
 * ```
 *
 * Cron and timezone are metadata for schedule providers. The inline runner only
 * runs schedules when its `run(...)` method is called.
 */
export function createSchedules<Ctx>(): Schedules<Ctx> {
  return {
    defineSchedule<Name extends string, Payload extends StandardSchema>(
      name: Name,
      options: DefineScheduleOptions<Name, Payload, Ctx>,
    ): ScheduleDef<Name, Payload, Ctx> {
      return defineScheduleImpl(name, options);
    },
  };
}
