import type { DesktopSshPasswordPromptRequest } from "@belweave/contracts";
import { DesktopSshPasswordPromptResolutionInputSchema } from "@belweave/contracts";
import type { SshPasswordRequest } from "@belweave/ssh/auth";
import * as Context from "effect/Context";
import * as Data from "effect/Data";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Random from "effect/Random";
import * as Ref from "effect/Ref";

import * as IpcChannels from "../ipc/channels.ts";
import * as ElectronWindow from "../electron/ElectronWindow.ts";

const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000;
const WINDOW_UNAVAILABLE_MESSAGE = "Trifecta window is not available for SSH authentication.";

type DesktopSshPasswordPromptResolutionInput =
  typeof DesktopSshPasswordPromptResolutionInputSchema.Type;

export class DesktopSshPromptUnavailableError extends Data.TaggedError(
  "DesktopSshPromptUnavailableError",
)<{
  readonly reason: string;
}> {
  override get message() {
    return this.reason;
  }
}

export class DesktopSshPromptWindowUnavailableError extends Data.TaggedError(
  "DesktopSshPromptWindowUnavailableError",
)<{
  readonly destination: string;
}> {
  override get message() {
    return WINDOW_UNAVAILABLE_MESSAGE;
  }
}

export class DesktopSshPromptSendError extends Data.TaggedError("DesktopSshPromptSendError")<{
  readonly requestId: string;
  readonly destination: string;
  readonly cause: unknown;
}> {
  override get message() {
    return WINDOW_UNAVAILABLE_MESSAGE;
  }
}

export class DesktopSshPromptTimedOutError extends Data.TaggedError(
  "DesktopSshPromptTimedOutError",
)<{
  readonly requestId: string;
  readonly destination: string;
}> {
  override get message() {
    return `SSH authentication timed out for ${this.destination}.`;
  }
}

export class DesktopSshPromptCancelledError extends Data.TaggedError(
  "DesktopSshPromptCancelledError",
)<{
  readonly requestId: string;
  readonly destination: string;
  readonly reason: string;
}> {
  override get message() {
    return this.reason;
  }
}

export class DesktopSshPromptInvalidRequestIdError extends Data.TaggedError(
  "DesktopSshPromptInvalidRequestIdError",
)<{
  readonly requestId: string;
}> {
  override get message() {
    return "Invalid SSH password prompt id.";
  }
}

export class DesktopSshPromptExpiredError extends Data.TaggedError("DesktopSshPromptExpiredError")<{
  readonly requestId: string;
}> {
  override get message() {
    return "SSH password prompt expired. Try connecting again.";
  }
}

export type DesktopSshPasswordPromptRequestError =
  | DesktopSshPromptUnavailableError
  | DesktopSshPromptWindowUnavailableError
  | DesktopSshPromptSendError
  | DesktopSshPromptTimedOutError
  | DesktopSshPromptCancelledError;

export type DesktopSshPasswordPromptResolveError =
  | DesktopSshPromptInvalidRequestIdError
  | DesktopSshPromptExpiredError;

export type DesktopSshPasswordPromptError =
  | DesktopSshPasswordPromptRequestError
  | DesktopSshPasswordPromptResolveError;

export function isDesktopSshPasswordPromptCancellation(
  error: unknown,
): error is DesktopSshPromptCancelledError | DesktopSshPromptTimedOutError {
  return (
    error instanceof DesktopSshPromptCancelledError ||
    error instanceof DesktopSshPromptTimedOutError
  );
}

export interface DesktopSshPasswordPromptsShape {
  readonly request: (
    request: SshPasswordRequest,
  ) => Effect.Effect<string, DesktopSshPasswordPromptRequestError>;
  readonly resolve: (
    input: DesktopSshPasswordPromptResolutionInput,
  ) => Effect.Effect<void, DesktopSshPasswordPromptResolveError>;
  readonly cancelPending: (reason: string) => Effect.Effect<void>;
}

export class DesktopSshPasswordPrompts extends Context.Service<
  DesktopSshPasswordPrompts,
  DesktopSshPasswordPromptsShape
>()("belweave/desktop/SshPasswordPrompts") {}

interface PendingSshPasswordPrompt {
  readonly requestId: string;
  readonly destination: string;
  readonly deferred: Deferred.Deferred<string, DesktopSshPasswordPromptRequestError>;
}

interface LayerOptions {
  readonly passwordPromptTimeoutMs?: number;
}

const removePending = (
  pendingRef: Ref.Ref<Map<string, PendingSshPasswordPrompt>>,
  requestId: string,
) =>
  Ref.modify(pendingRef, (pending) => {
    const entry = pending.get(requestId);
    if (entry === undefined) {
      return [Option.none<PendingSshPasswordPrompt>(), pending] as const;
    }

    const nextPending = new Map(pending);
    nextPending.delete(requestId);
    return [Option.some(entry), nextPending] as const;
  });

const failPending = (
  pending: PendingSshPasswordPrompt,
  error: DesktopSshPasswordPromptRequestError,
) => Deferred.fail(pending.deferred, error).pipe(Effect.asVoid);

const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) {
  const electronWindow = yield* ElectronWindow.ElectronWindow;
  const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>());
  const passwordPromptTimeoutMs =
    options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS;

  const cancelPending = (reason: string): Effect.Effect<void> =>
    Ref.getAndSet(pendingRef, new Map()).pipe(
      Effect.flatMap((pending) =>
        Effect.forEach(
          pending.values(),
          (entry) =>
            failPending(
              entry,
              new DesktopSshPromptCancelledError({
                requestId: entry.requestId,
                destination: entry.destination,
                reason,
              }),
            ),
          { discard: true },
        ),
      ),
      Effect.asVoid,
    );

  yield* Effect.addFinalizer(() =>
    cancelPending("SSH password prompt service stopped.").pipe(Effect.ignore),
  );

  const resolve = Effect.fn("desktop.sshPasswordPrompts.resolve")(function* (
    input: DesktopSshPasswordPromptResolutionInput,
  ): Effect.fn.Return<void, DesktopSshPasswordPromptResolveError> {
    const requestId = input.requestId.trim();
    if (requestId.length === 0) {
      return yield* new DesktopSshPromptInvalidRequestIdError({ requestId: input.requestId });
    }

    const pending = yield* removePending(pendingRef, requestId);
    if (Option.isNone(pending)) {
      return yield* new DesktopSshPromptExpiredError({ requestId });
    }

    const entry = pending.value;
    if (input.password === null) {
      yield* failPending(
        entry,
        new DesktopSshPromptCancelledError({
          requestId,
          destination: entry.destination,
          reason: `SSH authentication cancelled for ${entry.destination}.`,
        }),
      );
      return;
    }

    yield* Deferred.succeed(entry.deferred, input.password).pipe(Effect.asVoid);
  });

  const request = Effect.fn("desktop.sshPasswordPrompts.request")(function* (
    input: SshPasswordRequest,
  ): Effect.fn.Return<string, DesktopSshPasswordPromptRequestError> {
    const window = yield* electronWindow.main;
    if (Option.isNone(window) || window.value.isDestroyed()) {
      return yield* new DesktopSshPromptWindowUnavailableError({
        destination: input.destination,
      });
    }

    const requestId = yield* Random.nextUUIDv4;
    const now = yield* DateTime.now;
    const expiresAt = DateTime.formatIso(
      DateTime.add(now, { milliseconds: passwordPromptTimeoutMs }),
    );
    const promptRequest: DesktopSshPasswordPromptRequest = {
      requestId,
      destination: input.destination,
      username: input.username,
      prompt: input.prompt,
      expiresAt,
    };
    const deferred = yield* Deferred.make<string, DesktopSshPasswordPromptRequestError>();
    const pending: PendingSshPasswordPrompt = {
      requestId,
      destination: input.destination,
      deferred,
    };
    yield* Ref.update(pendingRef, (entries) => new Map(entries).set(requestId, pending));

    const context = yield* Effect.context();
    const runFork = Effect.runForkWith(context);

    const cancelOnWindowClosed = () => {
      runFork(
        removePending(pendingRef, requestId).pipe(
          Effect.flatMap((entry) =>
            Option.match(entry, {
              onNone: () => Effect.void,
              onSome: (pending) =>
                failPending(
                  pending,
                  new DesktopSshPromptCancelledError({
                    requestId,
                    destination: input.destination,
                    reason: "SSH authentication was cancelled because the app window closed.",
                  }),
                ),
            }),
          ),
        ),
      );
    };
    const cleanup = Effect.sync(() => {
      if (!window.value.isDestroyed()) {
        window.value.removeListener("closed", cancelOnWindowClosed);
      }
    }).pipe(Effect.andThen(removePending(pendingRef, requestId)), Effect.asVoid);
    const waitForPassword = Deferred.await(deferred).pipe(
      Effect.timeoutOption(Duration.millis(passwordPromptTimeoutMs)),
      Effect.flatMap(
        Option.match({
          onNone: () =>
            Effect.fail(
              new DesktopSshPromptTimedOutError({
                requestId,
                destination: input.destination,
              }),
            ),
          onSome: Effect.succeed,
        }),
      ),
    );

    return yield* Effect.try({
      try: () => {
        if (window.value.isDestroyed()) {
          throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
        }
        window.value.once("closed", cancelOnWindowClosed);
        window.value.webContents.send(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, promptRequest);
        if (window.value.isDestroyed()) {
          throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
        }
        if (window.value.isMinimized()) {
          window.value.restore();
        }
        if (window.value.isDestroyed()) {
          throw new Error(WINDOW_UNAVAILABLE_MESSAGE);
        }
        window.value.focus();
      },
      catch: (cause) =>
        new DesktopSshPromptSendError({
          requestId,
          destination: input.destination,
          cause,
        }),
    }).pipe(Effect.andThen(waitForPassword), Effect.ensuring(cleanup));
  });

  return DesktopSshPasswordPrompts.of({
    request,
    resolve,
    cancelPending,
  });
});

export const layer = (options: LayerOptions = {}) =>
  Layer.effect(DesktopSshPasswordPrompts, make(options));
