import { $hook, $inject, Alepha, AlephaError } from "alepha";
import { $logger } from "alepha/logger";
import type { CachePrimitive } from "../primitives/$cache.ts";
import { CacheProvider } from "./CacheProvider.ts";

// ---------------------------------------------------------------------------------------------------------------------

/**
 * KVNamespace interface matching Cloudflare's KV API.
 */
export interface KVNamespace {
  get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>;
  get(key: string, type: "text"): Promise<string | null>;
  get(key: string, type?: "text"): Promise<string | null>;
  put(
    key: string,
    value: string | ArrayBuffer | ReadableStream,
    options?: KVPutOptions,
  ): Promise<void>;
  delete(key: string): Promise<void>;
  list(options?: KVListOptions): Promise<KVListResult>;
}

export interface KVPutOptions {
  expiration?: number;
  expirationTtl?: number;
  metadata?: unknown;
}

export interface KVListOptions {
  prefix?: string;
  limit?: number;
  cursor?: string;
}

export interface KVListResult {
  keys: KVListKey[];
  list_complete: boolean;
  cursor?: string;
}

export interface KVListKey {
  name: string;
  expiration?: number;
  metadata?: unknown;
}

// ---------------------------------------------------------------------------------------------------------------------

/**
 * Default KV binding name used in wrangler configuration.
 */
export const KV_DEFAULT_BINDING = "KV_CACHE";

// ---------------------------------------------------------------------------------------------------------------------

/**
 * Cloudflare KV cache provider.
 *
 * Uses a KV namespace binding for all cache operations.
 * Keys are stored as: `cache:{name}:{key}`
 *
 * **Required Cloudflare binding:**
 * - `KV_CACHE` - A KV namespace binding in wrangler configuration
 *
 * @example
 * ```toml
 * # wrangler.toml - automatically generated by alepha build
 * [[kv_namespaces]]
 * binding = "KV_CACHE"
 * id = "abc123"
 * ```
 */
export class CloudflareKVProvider extends CacheProvider {
  protected readonly alepha = $inject(Alepha);
  protected readonly log = $logger();

  protected kv?: KVNamespace;

  protected readonly onStart = $hook({
    on: "start",
    handler: async () => {
      const caches = this.alepha
        .primitives<CachePrimitive>("cache")
        .filter((it) => it.provider === this);

      if (caches.length === 0) {
        this.log.info(
          "CloudflareKVProvider is registered but no cache primitives are using it. Skipping KV initialization.",
        );
        return;
      }

      const cloudflareEnv = this.alepha.get("cloudflare.env") as
        | Record<string, unknown>
        | undefined;
      if (!cloudflareEnv) {
        throw new AlephaError(
          "Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.",
        );
      }

      const binding = cloudflareEnv[KV_DEFAULT_BINDING] as
        | KVNamespace
        | undefined;
      if (!binding) {
        throw new AlephaError(
          `KV binding '${KV_DEFAULT_BINDING}' not found in Cloudflare Workers environment.`,
        );
      }

      this.kv = binding;

      this.log.info("Cloudflare KV cache OK");
    },
  });

  public async get(name: string, key: string): Promise<Uint8Array | undefined> {
    if (!this.alepha.isStarted()) {
      return;
    }

    const kvKey = this.prefix(name, key);
    const buffer = await this.getKV().get(kvKey, "arrayBuffer");
    if (!buffer) {
      return;
    }

    this.log.debug("Cache hit", {
      size: buffer.byteLength,
      key: kvKey,
    });

    return new Uint8Array(buffer);
  }

  public async set(
    name: string,
    key: string,
    value: Uint8Array,
    ttl?: number,
  ): Promise<Uint8Array> {
    if (!this.alepha.isReady()) {
      return new Uint8Array(value);
    }

    const kvKey = this.prefix(name, key);
    const options: KVPutOptions = {};

    if (ttl) {
      // KV expects TTL in seconds, we receive milliseconds
      options.expirationTtl = Math.max(60, Math.ceil(ttl / 1000));
    }

    await this.getKV().put(
      kvKey,
      value.buffer.slice(
        value.byteOffset,
        value.byteOffset + value.byteLength,
      ) as ArrayBuffer,
      options,
    );
    return value;
  }

  public async del(name: string, ...keys: string[]): Promise<void> {
    const kv = this.getKV();

    if (keys.length === 0) {
      // Delete all keys under this cache name
      const prefix = this.prefix(name);
      const allKeys = await this.listAllKeys(`${prefix}:`);
      for (const k of allKeys) {
        await kv.delete(k);
      }
      return;
    }

    const nameKey = this.prefix(name);
    for (const key of keys) {
      const fullKey = key.startsWith(nameKey) ? key : this.prefix(name, key);
      await kv.delete(fullKey);
    }
  }

  public async has(name: string, key: string): Promise<boolean> {
    const kvKey = this.prefix(name, key);
    const value = await this.getKV().get(kvKey, "text");
    return value !== null;
  }

  public async keys(name: string, filter?: string): Promise<string[]> {
    const prefix = filter
      ? `${this.prefix(name)}:${filter}`
      : `${this.prefix(name)}:`;

    return this.listAllKeys(prefix);
  }

  public async clear(): Promise<void> {
    this.log.debug("Clearing all cache");
    const kv = this.getKV();
    const allKeys = await this.listAllKeys("cache:");
    for (const key of allKeys) {
      await kv.delete(key);
    }
  }

  public async incr(
    name: string,
    key: string,
    amount: number,
  ): Promise<number> {
    const kvKey = this.prefix(name, key);
    const kv = this.getKV();

    const existing = await kv.get(kvKey, "text");
    let current = 0;

    if (existing !== null) {
      current = Number.parseInt(existing, 10) || 0;
    }

    const newValue = current + amount;
    await kv.put(kvKey, String(newValue));
    return newValue;
  }

  /**
   * Build the full KV key: `cache:{name}:{key}`
   */
  protected prefix(...path: string[]): string {
    return ["cache", ...path].join(":");
  }

  protected getKV(): KVNamespace {
    if (!this.kv) {
      throw new AlephaError(
        "KV namespace not initialized. Call start() first.",
      );
    }
    return this.kv;
  }

  /**
   * List all keys matching a prefix, handling pagination.
   */
  protected async listAllKeys(prefix: string): Promise<string[]> {
    const kv = this.getKV();
    const allKeys: string[] = [];
    let cursor: string | undefined;

    do {
      const result = await kv.list({
        prefix,
        cursor,
      });

      for (const key of result.keys) {
        allKeys.push(key.name);
      }

      cursor = result.list_complete ? undefined : result.cursor;
    } while (cursor);

    return allKeys;
  }
}
