import { createRouterClient } from "@orpc/server";
import { Cause, Effect, Exit, Hash, ManagedRuntime, Option } from "effect";
import type {
  AnyPlugin,
  AnyPluginConstructor,
  InferRegistryFromEntries,
  InitializedPlugin,
  LoadedPlugin,
  PluginConfigInput,
  PluginInstance,
  PluginRegistry,
  PluginRegistryEntry,
  PluginRouterType,
  PluginRuntimeConfig,
  RegisteredPlugin,
  RegisteredPlugins,
  UsePluginResult,
} from "../types";
import { PluginRuntimeError } from "./errors";
import { PluginService } from "./services/plugin.service";

export class PluginRuntime<R = RegisteredPlugins> {
  readonly __registryType?: R;

  private pluginCache = new Map<
    string,
    Effect.Effect<InitializedPlugin<AnyPlugin>, PluginRuntimeError>
  >();

  constructor(
    private runtime: ManagedRuntime.ManagedRuntime<PluginService, never>,
    private registry: PluginRegistry,
  ) {}

  private generateCacheKey(pluginId: string, config: unknown): string {
    const configHash = Hash.structure(config as object).toString();
    return `${pluginId}:${configHash}`;
  }

  private validatePluginId(pluginId: string): Effect.Effect<string, PluginRuntimeError> {
    if (!(pluginId in this.registry)) {
      return Effect.fail(
        new PluginRuntimeError({
          pluginId: String(pluginId),
          operation: "validate-plugin-id",
          cause: new Error(`Plugin ID '${String(pluginId)}' not found in registry.`),
          retryable: false,
        }),
      );
    }
    return Effect.succeed(String(pluginId));
  }

  private async runPromise<A, E>(effect: Effect.Effect<A, E, PluginService>): Promise<A> {
    const exit = await this.runtime.runPromiseExit(effect);

    if (Exit.isFailure(exit)) {
      const error = Cause.failureOption(exit.cause);
      if (Option.isSome(error)) {
        throw error.value;
      }
      throw Cause.squash(exit.cause);
    }

    return exit.value;
  }

  async usePlugin<K extends keyof R & string>(
    pluginId: K,
    config: PluginConfigInput<R[K]>,
    plugins?: Record<string, unknown>,
  ): Promise<UsePluginResult<K, R>> {
    const cacheKey = this.generateCacheKey(pluginId, { ...config, __plugins: plugins ?? {} });

    let cachedPlugin = this.pluginCache.get(cacheKey);
    if (!cachedPlugin) {
      const operation = Effect.gen(this, function* () {
        const pluginService = yield* PluginService;
        const validatedId = yield* this.validatePluginId(pluginId);

        // Load → Instantiate → Initialize
        const ctor = yield* pluginService.loadPlugin(validatedId);
        const instance = yield* pluginService.instantiatePlugin(pluginId, ctor);
        const initialized = yield* pluginService.initializePlugin(instance, config, plugins);

        return initialized;
      }).pipe(Effect.provide(this.runtime));

      cachedPlugin = Effect.cached(operation).pipe(Effect.flatten);
      this.pluginCache.set(cacheKey, cachedPlugin);
    }

    const initialized = await this.runPromise(cachedPlugin);

    // Create client factory that accepts request context
    const createClient = (context?: any) => {
      const router = initialized.plugin.createRouter(initialized.context);
      return createRouterClient(router, { context: context ?? {} });
    };

    return {
      createClient: createClient as any,
      router: initialized.plugin.createRouter(initialized.context) as PluginRouterType<R[K]>,
      metadata: initialized.metadata,
      initialized: initialized as InitializedPlugin<RegisteredPlugin<K, R>>,
    } as UsePluginResult<K, R>;
  }

  async loadPlugin<K extends keyof R & string>(
    pluginId: K,
  ): Promise<LoadedPlugin<RegisteredPlugin<K, R>>> {
    const effect = Effect.gen(function* () {
      const pluginService = yield* PluginService;
      return yield* pluginService.loadPlugin(pluginId);
    });
    return this.runPromise(effect) as Promise<LoadedPlugin<RegisteredPlugin<K, R>>>;
  }

  async instantiatePlugin<K extends keyof R & string>(
    pluginId: K,
    loadedPlugin: LoadedPlugin<RegisteredPlugin<K, R>>,
  ): Promise<PluginInstance<RegisteredPlugin<K, R>>> {
    const effect = Effect.gen(function* () {
      const pluginService = yield* PluginService;
      return yield* pluginService.instantiatePlugin(pluginId, loadedPlugin);
    });
    return this.runPromise(effect) as Promise<PluginInstance<RegisteredPlugin<K, R>>>;
  }

  async initializePlugin<T extends AnyPlugin>(
    instance: PluginInstance<T>,
    config: any,
    plugins?: Record<string, unknown>,
  ): Promise<InitializedPlugin<T>> {
    const effect = Effect.gen(function* () {
      const pluginService = yield* PluginService;
      return yield* pluginService.initializePlugin(instance, config, plugins);
    });
    return this.runPromise(effect);
  }

  async shutdown(): Promise<void> {
    const effect = Effect.gen(function* () {
      const pluginService = yield* PluginService;
      yield* pluginService.cleanup();
    });
    await this.runPromise(effect);
    await this.runtime.dispose();
  }

  async evictPlugin<K extends keyof R & string>(
    pluginId: K,
    config: PluginConfigInput<R[K]>,
  ): Promise<void> {
    const cacheKey = this.generateCacheKey(pluginId, config);

    const effect = Effect.gen(this, function* () {
      const pluginService = yield* PluginService;
      const cachedPlugin = this.pluginCache.get(cacheKey);

      if (cachedPlugin) {
        this.pluginCache.delete(cacheKey);

        const pluginResult = yield* cachedPlugin.pipe(Effect.catchAll(() => Effect.succeed(null)));

        if (pluginResult) {
          yield* pluginService
            .shutdownPlugin(pluginResult)
            .pipe(
              Effect.catchAll((error) =>
                Effect.logWarning(`Failed to shutdown evicted plugin ${pluginId}`, error),
              ),
            );
        }
      }
    }).pipe(
      Effect.catchAll((error) =>
        Effect.logWarning(`Plugin eviction failed for ${pluginId}`, error),
      ),
    );

    return this.runPromise(effect);
  }
}

/**
 * Normalizes a remote URL to ensure it points to remoteEntry.js
 * If the URL doesn't end with a file extension, appends /remoteEntry.js
 */
function normalizeRemoteUrl(url: string): string {
  if (!url) return url;
  if (url.endsWith(".js") || url.endsWith(".json")) return url;
  return `${url.endsWith("/") ? url.slice(0, -1) : url}/remoteEntry.js`;
}

/**
 * Extract plugin map (module constructors) from registry entries
 */
function extractPluginMap(
  registry: Record<string, PluginRegistryEntry>,
): Record<string, AnyPluginConstructor> {
  const pluginMap: Record<string, AnyPluginConstructor> = {};

  for (const [pluginId, entry] of Object.entries(registry)) {
    if ("module" in entry && entry.module) {
      pluginMap[pluginId] = entry.module;
    }
  }

  return pluginMap;
}

/**
 * Normalize registry entries - ensure remote URLs are properly formatted
 */
function normalizeRegistry(registry: Record<string, PluginRegistryEntry>): PluginRegistry {
  const normalized: Record<string, PluginRegistryEntry> = {};

  for (const [pluginId, entry] of Object.entries(registry)) {
    if ("module" in entry) {
      normalized[pluginId] = {
        ...entry,
        remote: entry.remote ? normalizeRemoteUrl(entry.remote) : undefined,
      };
    } else {
      normalized[pluginId] = {
        ...entry,
        remote: normalizeRemoteUrl(entry.remote),
      };
    }
  }

  return normalized as PluginRegistry;
}

/**
 * Creates a plugin runtime with support for both module and remote plugin entries.
 *
 * @example
 * ```typescript
 * // With module entries (types inferred automatically)
 * const runtime = createPluginRuntime({
 *   registry: {
 *     telegram: { module: TelegramPlugin },
 *     gopher: { remote: "https://cdn.example.com/gopher/remoteEntry.js" }
 *   },
 *   secrets: { API_KEY: "..." }
 * });
 *
 * // Types are automatically inferred from module entries!
 * const { router } = await runtime.usePlugin("telegram", config);
 * ```
 */
export function createPluginRuntime<TRegistry extends Record<string, PluginRegistryEntry>>(
  config: PluginRuntimeConfig<TRegistry>,
): PluginRuntime<InferRegistryFromEntries<TRegistry>> {
  const secrets = config.secrets || {};
  const normalizedRegistry = normalizeRegistry(config.registry);
  const pluginMap = extractPluginMap(config.registry);

  const layer = PluginService.Live(normalizedRegistry, secrets, pluginMap);
  const runtime = ManagedRuntime.make(layer);

  return new PluginRuntime(runtime, normalizedRegistry) as PluginRuntime<
    InferRegistryFromEntries<TRegistry>
  >;
}
