import { Context, Duration, Effect, Semaphore } from "effect";
import * as op from "@1password/op-js";

import { OnePasswordError } from "./errors";
import type { OnePasswordSdkModule } from "./onepassword-sdk";

// ---------------------------------------------------------------------------
// Canonical service interface — all backends (SDK, CLI) implement this
// ---------------------------------------------------------------------------

export interface OnePasswordVault {
  readonly id: string;
  readonly title: string;
}

export interface OnePasswordItem {
  readonly id: string;
  readonly title: string;
}

export interface OnePasswordService {
  /** Resolve a secret by op:// URI */
  readonly resolveSecret: (uri: string) => Effect.Effect<string, OnePasswordError>;

  /** List accessible vaults */
  readonly listVaults: () => Effect.Effect<ReadonlyArray<OnePasswordVault>, OnePasswordError>;

  /** List items in a vault */
  readonly listItems: (
    vaultId: string,
  ) => Effect.Effect<ReadonlyArray<OnePasswordItem>, OnePasswordError>;
}

export class OnePasswordServiceTag extends Context.Service<
  OnePasswordServiceTag,
  OnePasswordService
>()("@executor-js/plugin-onepassword/OnePasswordService") {}

// ---------------------------------------------------------------------------
// Resolved auth — raw credentials ready for any backend
// ---------------------------------------------------------------------------

export type ResolvedAuth =
  | { readonly kind: "desktop-app"; readonly accountName: string }
  | { readonly kind: "service-account"; readonly token: string };

// ---------------------------------------------------------------------------
// SDK backend — uses @1password/sdk native IPC
// ---------------------------------------------------------------------------

const DEFAULT_TIMEOUT_MS = 15_000;
const MAX_ERROR_MESSAGE_LENGTH = 300;
const SERVICE_ACCOUNT_TOKEN_RE = /ops_[A-Za-z0-9_-]+/g;

const formatCause = (cause: unknown): string => {
  // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: normalizing untyped op-js/SDK throwables into OnePasswordError.message
  const maybeMessage = (cause as { readonly message?: unknown } | null | undefined)?.message;
  const raw =
    // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: last-resort stringification of a non-Error throwable
    typeof maybeMessage === "string" && maybeMessage.length > 0 ? maybeMessage : String(cause);
  return raw
    .replace(SERVICE_ACCOUNT_TOKEN_RE, "[redacted 1Password token]")
    .replace(/\s+/g, " ")
    .trim();
};

const messageWithCause = (prefix: string, cause: unknown): string => {
  const causeMessage = formatCause(cause);
  const message = causeMessage ? `${prefix}: ${causeMessage}` : prefix;
  return message.length > MAX_ERROR_MESSAGE_LENGTH
    ? `${message.slice(0, MAX_ERROR_MESSAGE_LENGTH - 3)}...`
    : message;
};

const hasOnePasswordSdkShape = (value: unknown): value is OnePasswordSdkModule => {
  const sdk = value as Partial<OnePasswordSdkModule> | null | undefined;
  return typeof sdk?.createClient === "function" && typeof sdk.DesktopAuth === "function";
};

const invalidOnePasswordSdkError = () =>
  new OnePasswordError({
    operation: "sdk module load",
    message: [
      "Failed to load 1Password SDK: the packaged SDK module did not expose createClient and DesktopAuth.",
      "Install the 1Password CLI (`op`) in /opt/homebrew/bin or /usr/local/bin and retry, or update Executor.",
    ].join(" "),
  });

const loadOnePasswordSdk = (): Effect.Effect<OnePasswordSdkModule, OnePasswordError> =>
  Effect.tryPromise({
    try: () => import("./onepassword-sdk").then((module) => module.onePasswordSdk),
    catch: (cause) =>
      new OnePasswordError({
        operation: "sdk module load",
        message: messageWithCause("Failed to load 1Password SDK", cause),
      }),
  }).pipe(
    Effect.flatMap((sdk) =>
      hasOnePasswordSdkShape(sdk) ? Effect.succeed(sdk) : Effect.fail(invalidOnePasswordSdkError()),
    ),
  );

const makeTimeoutMessage = (operation: string, timeoutMs: number): string =>
  [
    `${operation}: timed out after ${Math.floor(timeoutMs / 1000)}s.`,
    "Troubleshooting:",
    "1. Make sure the 1Password desktop app is open and unlocked",
    "2. Check for an approval prompt in the 1Password app — it may be behind other windows",
    "3. Ensure 'Developer > Connect with 1Password CLI' is enabled in 1Password Settings",
    "4. Make sure no other app or terminal is waiting for 1Password approval (only one prompt at a time)",
    "5. Try quitting 1Password completely and reopening it, then retry",
  ].join("\n");

const timeoutWithOnePasswordError = (operation: string, timeoutMs: number) =>
  Effect.timeoutOrElse({
    duration: Duration.millis(timeoutMs),
    orElse: () =>
      Effect.fail(
        new OnePasswordError({
          operation,
          message: makeTimeoutMessage(operation, timeoutMs),
        }),
      ),
  });

export const makeNativeSdkService = (
  auth: ResolvedAuth,
  timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Effect.Effect<OnePasswordService, OnePasswordError> =>
  Effect.gen(function* () {
    const sdk = yield* loadOnePasswordSdk().pipe(
      timeoutWithOnePasswordError("sdk module load", timeoutMs),
    );

    const client = yield* Effect.tryPromise({
      try: () =>
        sdk.createClient({
          auth: auth.kind === "desktop-app" ? new sdk.DesktopAuth(auth.accountName) : auth.token,
          integrationName: "Executor",
          integrationVersion: "0.0.0",
        }),
      catch: (cause) =>
        new OnePasswordError({
          operation: "client setup",
          message: messageWithCause("Failed to set up 1Password client", cause),
        }),
    }).pipe(timeoutWithOnePasswordError("client setup", timeoutMs));

    const wrap = <A>(fn: () => Promise<A>, operation: string): Effect.Effect<A, OnePasswordError> =>
      Effect.tryPromise({
        try: fn,
        catch: (cause) =>
          new OnePasswordError({
            operation,
            message: messageWithCause(`1Password SDK ${operation} failed`, cause),
          }),
      }).pipe(
        timeoutWithOnePasswordError(operation, timeoutMs),
        Effect.withSpan(`onepassword.sdk.${operation}`),
      );

    return OnePasswordServiceTag.of({
      resolveSecret: (uri) => wrap(() => client.secrets.resolve(uri), "secret resolution"),

      listVaults: () =>
        wrap(() => client.vaults.list({ decryptDetails: true }), "vault listing").pipe(
          Effect.map((vaults) => vaults.map((v) => ({ id: v.id, title: v.title }))),
        ),

      listItems: (vaultId) =>
        wrap(() => client.items.list(vaultId), "item listing").pipe(
          Effect.map((items) => items.map((i) => ({ id: i.id, title: i.title }))),
        ),
    });
  }).pipe(Effect.withSpan("onepassword.sdk.make_service"));

// ---------------------------------------------------------------------------
// CLI backend — uses @1password/op-js (shells out to `op` CLI)
// ---------------------------------------------------------------------------

const cliAuthLock = Semaphore.makeUnsafe(1);

export const makeCliService = (
  auth: ResolvedAuth,
): Effect.Effect<OnePasswordService, OnePasswordError> =>
  Effect.sync(() => {
    const wrapSync = <A>(fn: () => A, operation: string): Effect.Effect<A, OnePasswordError> =>
      cliAuthLock
        .withPermits(1)(
          Effect.try({
            try: () => {
              if (auth.kind === "service-account") {
                op.setGlobalFlags({});
                op.setServiceAccount(auth.token);
              } else {
                op.setServiceAccount("");
                op.setGlobalFlags({ account: auth.accountName });
              }
              return fn();
            },
            catch: (cause) =>
              new OnePasswordError({
                operation,
                message: messageWithCause(`1Password CLI ${operation} failed`, cause),
              }),
          }),
        )
        .pipe(Effect.withSpan(`onepassword.cli.${operation}`));

    return OnePasswordServiceTag.of({
      resolveSecret: (uri) => wrapSync(() => op.read.parse(uri), "secret resolution"),

      listVaults: () =>
        wrapSync(() => op.vault.list(), "vault listing").pipe(
          Effect.map((vaults) => vaults.map((v) => ({ id: v.id, title: v.name }))),
        ),

      listItems: (vaultId) =>
        wrapSync(() => op.item.list({ vault: vaultId }), "item listing").pipe(
          Effect.map((items) => items.map((i) => ({ id: i.id, title: i.title }))),
        ),
    });
  }).pipe(Effect.withSpan("onepassword.cli.make_service"));

// ---------------------------------------------------------------------------
// Smart factory — tries CLI first (avoids IPC hang), falls back to SDK
// ---------------------------------------------------------------------------

const isCliUnavailable = (error: OnePasswordError): boolean => {
  // oxlint-disable-next-line executor/no-unknown-error-message -- boundary: OnePasswordError carries a typed `message`
  const message = error.message.toLowerCase();
  return (
    message.includes("enoent") ||
    message.includes("not found") ||
    message.includes("command not found") ||
    message.includes("not installed") ||
    message.includes("no such file") ||
    message.includes("spawn op")
  );
};

const chooseFallbackError = (
  cliError: OnePasswordError,
  sdkError: OnePasswordError,
): OnePasswordError => (isCliUnavailable(cliError) ? sdkError : cliError);

export const makeOnePasswordService = (
  auth: ResolvedAuth,
  options?: { readonly preferSdk?: boolean; readonly timeoutMs?: number },
): Effect.Effect<OnePasswordService, OnePasswordError> => {
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;

  if (options?.preferSdk) {
    return makeNativeSdkService(auth, timeoutMs);
  }

  return Effect.gen(function* () {
    const cliService = yield* makeCliService(auth);
    const sdkService = yield* Effect.cached(makeNativeSdkService(auth, timeoutMs));

    const withSdkFallback = <A>(
      cliEffect: Effect.Effect<A, OnePasswordError>,
      sdkEffect: (service: OnePasswordService) => Effect.Effect<A, OnePasswordError>,
    ): Effect.Effect<A, OnePasswordError> =>
      cliEffect.pipe(
        Effect.catch((cliError: OnePasswordError) =>
          sdkService.pipe(
            Effect.flatMap(sdkEffect),
            Effect.mapError((sdkError: OnePasswordError) =>
              chooseFallbackError(cliError, sdkError),
            ),
          ),
        ),
      );

    return OnePasswordServiceTag.of({
      resolveSecret: (uri) =>
        withSdkFallback(cliService.resolveSecret(uri), (service) => service.resolveSecret(uri)),

      listVaults: () => withSdkFallback(cliService.listVaults(), (service) => service.listVaults()),

      listItems: (vaultId) =>
        withSdkFallback(cliService.listItems(vaultId), (service) => service.listItems(vaultId)),
    });
  }).pipe(Effect.withSpan("onepassword.make_service"));
};
