import { Data, Schema } from "effect";

import { ConnectionId, ToolId, SecretId } from "./ids";

// ---------------------------------------------------------------------------
// Tool lifecycle
// ---------------------------------------------------------------------------

export class ToolNotFoundError extends Schema.TaggedErrorClass<ToolNotFoundError>()(
  "ToolNotFoundError",
  {
    toolId: ToolId,
    suggestions: Schema.optional(Schema.Array(ToolId)),
  },
) {}

export class ToolInvocationError extends Data.TaggedError("ToolInvocationError")<{
  readonly toolId: ToolId;
  readonly message: string;
  readonly cause?: unknown;
}> {}

/** Tool row exists in the DB but its owning plugin isn't loaded. Means
 *  the tool was registered by a plugin that's no longer present in the
 *  current executor config — usually a stale row from an older session. */
export class PluginNotLoadedError extends Schema.TaggedErrorClass<PluginNotLoadedError>()(
  "PluginNotLoadedError",
  {
    pluginId: Schema.String,
    toolId: ToolId,
  },
) {}

/** Tool was found but its owning plugin has no `invokeTool` handler —
 *  the plugin only declares static tools and this one's id matched
 *  dynamically somehow. Shouldn't happen in practice; guards against
 *  programmer error. */
export class NoHandlerError extends Schema.TaggedErrorClass<NoHandlerError>()("NoHandlerError", {
  toolId: ToolId,
  pluginId: Schema.String,
}) {}

/** Tool invocation was rejected because a workspace `tool_policy` rule
 *  with `action: "block"` matched. `pattern` is the matched policy
 *  pattern so callers / agents can render a useful "this is blocked
 *  by your `vercel.dns.*` rule" message. */
export class ToolBlockedError extends Schema.TaggedErrorClass<ToolBlockedError>()(
  "ToolBlockedError",
  {
    toolId: ToolId,
    pattern: Schema.String,
  },
) {}

// ---------------------------------------------------------------------------
// Source lifecycle
// ---------------------------------------------------------------------------

export class SourceNotFoundError extends Schema.TaggedErrorClass<SourceNotFoundError>()(
  "SourceNotFoundError",
  { sourceId: Schema.String },
) {}

/** `executor.sources.remove({ id, targetScope })` was called on a
 *  source with `canRemove: false` — typically a static source declared
 *  by a plugin at startup. Removing static sources is a bug in the
 *  caller. */
export class SourceRemovalNotAllowedError extends Schema.TaggedErrorClass<SourceRemovalNotAllowedError>()(
  "SourceRemovalNotAllowedError",
  { sourceId: Schema.String },
) {}

// ---------------------------------------------------------------------------
// Secrets
// ---------------------------------------------------------------------------

export class SecretNotFoundError extends Schema.TaggedErrorClass<SecretNotFoundError>()(
  "SecretNotFoundError",
  { secretId: SecretId },
) {}

export class SecretResolutionError extends Schema.TaggedErrorClass<SecretResolutionError>()(
  "SecretResolutionError",
  {
    secretId: SecretId,
    message: Schema.String,
  },
) {}

/** Raised when `secrets.remove({ id, targetScope })` is called on a secret whose row has
 *  `owned_by_connection_id` set. The connection owns the lifecycle —
 *  callers must go through `connections.remove(connectionId)` to
 *  delete it along with its siblings. */
export class SecretOwnedByConnectionError extends Schema.TaggedErrorClass<SecretOwnedByConnectionError>()(
  "SecretOwnedByConnectionError",
  {
    secretId: SecretId,
    connectionId: ConnectionId,
  },
) {}

/** Raised when `secrets.remove({ id, targetScope })` is called on a secret that's still
 *  referenced by one or more sources / bindings across plugins. The UI's
 *  "Used by" list tells the user which sources to detach first. App-
 *  level RESTRICT — the codebase doesn't enforce DB-level FKs because
 *  composite `(scope_id, id)` PKs make single-column references
 *  impossible to constrain in sqlite. `usageCount` is a hint for the
 *  caller; the full list is queryable via `secrets.usages(id)`. */
export class SecretInUseError extends Schema.TaggedErrorClass<SecretInUseError>()(
  "SecretInUseError",
  {
    secretId: SecretId,
    usageCount: Schema.Number,
  },
) {}

// ---------------------------------------------------------------------------
// Connections
// ---------------------------------------------------------------------------

export class ConnectionNotFoundError extends Schema.TaggedErrorClass<ConnectionNotFoundError>()(
  "ConnectionNotFoundError",
  { connectionId: ConnectionId },
) {}

export class ConnectionProviderNotRegisteredError extends Schema.TaggedErrorClass<ConnectionProviderNotRegisteredError>()(
  "ConnectionProviderNotRegisteredError",
  {
    provider: Schema.String,
    connectionId: Schema.optional(ConnectionId),
  },
) {}

export class ConnectionRefreshNotSupportedError extends Schema.TaggedErrorClass<ConnectionRefreshNotSupportedError>()(
  "ConnectionRefreshNotSupportedError",
  {
    connectionId: ConnectionId,
    provider: Schema.String,
  },
) {}

/**
 * Raised by `connections.accessToken(id)` when the provider's refresh
 * handler reported that the stored refresh token is permanently
 * invalid (RFC 6749 §5.2 `invalid_grant` and friends). The caller —
 * typically a tool invocation — surfaces this so the UI can prompt the
 * user to sign in again. Distinct from `ConnectionRefreshError` so
 * "the network flaked, retry later" and "the grant is dead, re-auth"
 * don't collapse into one error tag at the plugin boundary.
 */
export class ConnectionReauthRequiredError extends Schema.TaggedErrorClass<ConnectionReauthRequiredError>()(
  "ConnectionReauthRequiredError",
  {
    connectionId: ConnectionId,
    provider: Schema.String,
    message: Schema.String,
  },
) {}

/** Raised when `connections.remove(id)` is called on a connection that's
 *  still referenced by sources / bindings. Mirrors `SecretInUseError`. */
export class ConnectionInUseError extends Schema.TaggedErrorClass<ConnectionInUseError>()(
  "ConnectionInUseError",
  {
    connectionId: ConnectionId,
    usageCount: Schema.Number,
  },
) {}

// ---------------------------------------------------------------------------
// Union type for convenience in signatures.
// ---------------------------------------------------------------------------

export type ExecutorError =
  | ToolNotFoundError
  | ToolInvocationError
  | PluginNotLoadedError
  | NoHandlerError
  | ToolBlockedError
  | SourceNotFoundError
  | SourceRemovalNotAllowedError
  | SecretNotFoundError
  | SecretResolutionError
  | SecretOwnedByConnectionError
  | SecretInUseError
  | ConnectionNotFoundError
  | ConnectionProviderNotRegisteredError
  | ConnectionRefreshNotSupportedError
  | ConnectionReauthRequiredError
  | ConnectionInUseError;
