import type { Context } from "@wrnexus/core";
import { invalid, parseBody, type ObjectSchema } from "@wrnexus/validation";
import type { AuthEngine } from "../engine.ts";
import { safeAuthReturnTo } from "../normalize.ts";
import {
  clearAuthSession,
  establishAuthSession,
  getAuthSession,
  getAuthUser,
} from "../middleware.ts";
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
import type {
  AuthSessionVerificationHandler,
  AuthSignedInHandler,
  AuthSignedOutHandler,
} from "../types.ts";

function text(value: unknown): string {
  return typeof value === "string" ? value : value == null ? "" : String(value);
}

function boolean(value: unknown): boolean {
  return value === true || value === "true" || value === "on" || value === "1";
}

function json(data: unknown, status = 200): Response {
  return Response.json(data, {
    status,
    headers: { "cache-control": "no-store" },
  });
}

function parseValues<T = Record<string, unknown>>(
  schema: ObjectSchema,
  input: Record<string, unknown>,
): { ok: true; value: T } | { ok: false; response: Response } {
  const result = schema.parse(input);
  if (!result.ok) return { ok: false, response: invalid(result.errors) };
  return { ok: true, value: result.value as T };
}

export interface AuthPasskeyHttpOptions {
  /** Relying-party ID accepted by the server. Defaults to the request hostname. */
  rpId?: string;
  /** Display name included in registration options. */
  rpName?: string;
  /** Exact WebAuthn origin accepted by the server. Defaults to the request origin. */
  origin?: string;
}

export interface AuthHttpOptions {
  engine: AuthEngine;
  baseUrl?: string;
  schemas?: AuthSchemaOverrides | AuthSchemaSet;
  passkey?: AuthPasskeyHttpOptions;
  /** @deprecated Prefer createAuthEngine({ onSignedIn }). */
  onSignedIn?: AuthSignedInHandler;
  /** @deprecated Prefer createAuthEngine({ onSignedOut }). */
  onSignedOut?: AuthSignedOutHandler;
  onSessionVerification?: AuthSessionVerificationHandler;
}

export function createAuthHttpHandlers(options: AuthHttpOptions) {
  const engine = options.engine;
  const schemas = resolveAuthSchemas(options.schemas);
  const onSignedIn = options.onSignedIn ?? engine.onSignedIn;
  const onSignedOut = options.onSignedOut ?? engine.onSignedOut;
  const onSuccessfulSignUp = engine.onSuccessfulSignUp;

  function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response {
    const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback;
    return Response.redirect(new URL(path, ctx.url), 303);
  }

  function passkeyConfig(ctx: Context): { rpId: string; rpName: string; origin: string } {
    const origin = options.passkey?.origin ?? ctx.url.origin;
    let originHostname = ctx.url.hostname;
    try {
      originHostname = new URL(origin).hostname;
    } catch {
      // Configuration validation belongs to application startup; retain a safe request fallback.
    }
    return {
      rpId: options.passkey?.rpId ?? originHostname,
      rpName: options.passkey?.rpName ?? "WRNexusJS",
      origin,
    };
  }

  return {
    async verifySession(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (options.onSessionVerification) {
        return options.onSessionVerification(ctx, user);
      }
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      if ((ctx.req.headers.get("accept") ?? "").includes("application/json")) {
        return json({ ok: true, user });
      }
      return new Response(null, {
        status: 204,
        headers: { "cache-control": "no-store" },
      });
    },

    async register(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.register, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const result = await engine.register({
        email: text(input.email) || undefined,
        phone: text(input.phone) || undefined,
        username: text(input.username) || undefined,
        password: text(input.password),
        displayName: text(input.displayName) || undefined,
        locale: text(input.locale) || undefined,
        timezone: text(input.timezone) || undefined,
      });
      if (!result.ok || !result.user) return json(result, 400);

      const action = await onSuccessfulSignUp?.(ctx, result.user);
      if (action instanceof Response) return action;
      if (action?.autoSignIn) {
        const identifier = text(input.email) || text(input.phone) || text(input.username);
        const loginResult = await engine.login({
          identifier,
          password: text(input.password),
          ip: ctx.ip,
          userAgent: ctx.req.headers.get("user-agent") ?? undefined,
          captchaVerified:
            Boolean((ctx.locals.captcha as { success?: boolean } | undefined)?.success) ||
            ctx.locals.captchaVerified === true,
        });
        if (!loginResult.ok || !loginResult.session || !loginResult.user) {
          return json(loginResult, 401);
        }
        establishAuthSession(ctx, loginResult.session, loginResult.user);
        return signupRedirect(ctx, action.redirectTo, "/account");
      }
      return signupRedirect(ctx, action?.redirectTo, "/sign-in");
    },

    async login(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.login, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const result = await engine.login({
        identifier: text(input.identifier),
        password: text(input.password),
        ip: ctx.ip,
        userAgent: ctx.req.headers.get("user-agent") ?? undefined,
        fingerprint: text(input.deviceFingerprint) || undefined,
        deviceName: text(input.deviceName) || undefined,
        rememberDevice: boolean(input.rememberDevice),
        captchaVerified:
          Boolean((ctx.locals.captcha as { success?: boolean } | undefined)?.success) ||
          ctx.locals.captchaVerified === true,
        signals: {
          automationSuspected: Boolean(
            ctx.locals.captchaRisk && (ctx.locals.captchaRisk as { challenge?: boolean }).challenge,
          ),
        },
      });
      if (!result.ok || !result.session || !result.user) return json(result, 401);
      establishAuthSession(ctx, result.session, result.user);
      if (onSignedIn) {
        return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin));
      }
      return json(result);
    },

    async logout(ctx: Context): Promise<Response> {
      const session = getAuthSession(ctx);
      if (session) await engine.logout(session.id);
      clearAuthSession(ctx);
      return onSignedOut ? onSignedOut(ctx) : json({ ok: true });
    },

    async requestVerification(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.verificationRequest, ctx.req);
      if (!validation.ok) return validation.response;
      const type = text(validation.value.type) === "phone" ? "phone" : "email";
      const current = getAuthUser(ctx);
      const identifier = text(validation.value.identifier);
      const user =
        current ?? (identifier ? await engine.findUserByIdentifier(identifier) : undefined);
      if (user) {
        await engine.requestVerification(user.id, type, options.baseUrl ?? ctx.url.origin);
      }
      // Always return the same response to avoid revealing account existence.
      return json({ ok: true });
    },

    async verifyEmail(ctx: Context): Promise<Response> {
      const validation =
        ctx.req.method === "GET"
          ? parseValues(schemas.verificationToken, {
              token: ctx.url.searchParams.get("token"),
            })
          : await parseBody(schemas.verificationToken, ctx.req);
      if (!validation.ok) return validation.response;
      const result = await engine.verifyEmail(text(validation.value.token));
      return json(result, result.ok ? 200 : 400);
    },

    async verifyPhone(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.verificationToken, ctx.req);
      if (!validation.ok) return validation.response;
      const result = await engine.verifyPhone(text(validation.value.token));
      return json(result, result.ok ? 200 : 400);
    },

    async requestPasswordReset(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.passwordResetRequest, ctx.req);
      if (!validation.ok) return validation.response;
      await engine.requestPasswordReset(
        text(validation.value.identifier),
        options.baseUrl ?? ctx.url.origin,
      );
      return json({ ok: true });
    },

    async resetPassword(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.passwordReset, ctx.req);
      if (!validation.ok) return validation.response;
      const result = await engine.resetPassword(
        text(validation.value.token),
        text(validation.value.password),
      );
      return json(result, result.ok ? 200 : 400);
    },

    async acceptInvitation(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.invitationAccept, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const result = await engine.acceptInvitation(text(input.token), {
        password: text(input.password) || undefined,
        displayName: text(input.displayName) || undefined,
      });
      return json(result, result.ok ? 200 : 400);
    },

    async requestMagicLink(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.magicLinkRequest, ctx.req);
      if (!validation.ok) return validation.response;
      await engine.requestMagicLink(
        text(validation.value.identifier),
        options.baseUrl ?? ctx.url.origin,
      );
      return json({ ok: true });
    },

    async consumeMagicLink(ctx: Context): Promise<Response> {
      const validation =
        ctx.req.method === "GET"
          ? parseValues(schemas.magicLinkConsume, {
              token: ctx.url.searchParams.get("token"),
              returnTo: ctx.url.searchParams.get("returnTo") ?? undefined,
            })
          : await parseBody(schemas.magicLinkConsume, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const result = await engine.consumeMagicLink(text(input.token), {
        ip: ctx.ip,
        userAgent: ctx.req.headers.get("user-agent") ?? undefined,
      });
      if (result.code === "mfa-required") return json(result, 401);
      if (!result.ok || !result.session || !result.user) return json(result, 400);
      establishAuthSession(ctx, result.session, result.user);
      if (onSignedIn) {
        return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin));
      }
      return json(result);
    },

    async requestOtpLogin(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.otpLoginRequest, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const method = text(input.method) === "sms-otp" ? "sms-otp" : "email-otp";
      const challenge = await engine.requestOtpLogin(text(input.identifier), method);
      return json({ ok: true, challenge: challenge ?? null });
    },

    async completeOtpLogin(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.otpLoginComplete, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const result = await engine.completeOtpLogin(text(input.challengeId), text(input.code), {
        ip: ctx.ip,
        userAgent: ctx.req.headers.get("user-agent") ?? undefined,
      });
      if (result.code === "mfa-required") return json(result, 401);
      if (!result.ok || !result.session || !result.user) return json(result, 400);
      establishAuthSession(ctx, result.session, result.user);
      if (onSignedIn) {
        return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin));
      }
      return json(result);
    },

    async issueOtp(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.otpIssue, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const method = text(input.method) === "sms-otp" ? "sms-otp" : "email-otp";
      try {
        return json({
          ok: true,
          ...(await engine.issueOtp(user.id, method, text(input.destination) || undefined)),
        });
      } catch (error) {
        return json(
          {
            ok: false,
            error: error instanceof Error ? error.message : "Unable to issue OTP",
          },
          400,
        );
      }
    },

    async verifyOtp(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.otpVerify, ctx.req);
      if (!validation.ok) return validation.response;
      const result = await engine.verifyOtp(
        text(validation.value.challengeId),
        text(validation.value.code),
        "verification",
      );
      return json(result, result.ok ? 200 : 400);
    },

    async beginTotp(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.authenticatorSetup, ctx.req);
      if (!validation.ok) return validation.response;
      const setup = await engine.beginTotp(
        user.id,
        text(validation.value.label) || "Authenticator",
      );
      return json({ ok: true, ...setup });
    },

    async confirmTotp(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.authenticatorConfirm, ctx.req);
      if (!validation.ok) return validation.response;
      const ok = await engine.confirmTotp(
        user.id,
        text(validation.value.credentialId),
        text(validation.value.code),
      );
      return ok
        ? json({ ok: true })
        : json({ ok: false, error: "Authenticator code is invalid" }, 400);
    },

    async disableTotp(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.authenticatorDisable, ctx.req);
      if (!validation.ok) return validation.response;
      const ok = await engine.disableTotp(user.id, text(validation.value.credentialId));
      return ok
        ? json({ ok: true })
        : json({ ok: false, error: "Authenticator credential was not found" }, 404);
    },

    async recoveryCodes(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.recoveryCodes, ctx.req);
      if (!validation.ok) return validation.response;
      const count = typeof validation.value.count === "number" ? validation.value.count : undefined;
      const codes = await engine.generateRecoveryCodes(user.id, count);
      return json({ ok: true, codes });
    },

    async changePassword(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.changePassword, ctx.req);
      if (!validation.ok) return validation.response;
      const result = await engine.changePassword(
        user.id,
        text(validation.value.currentPassword),
        text(validation.value.nextPassword),
      );
      if (result.ok) clearAuthSession(ctx);
      return json(result, result.ok ? 200 : 400);
    },

    async beginMfaOtp(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.mfaOtpRequest, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const method = text(input.method) === "sms-otp" ? "sms-otp" : "email-otp";
      const challenge = await engine.beginMfaOtp(text(input.mfaToken), method);
      return challenge
        ? json({ ok: true, ...challenge })
        : json({ ok: false, error: "MFA transaction is invalid or expired" }, 400);
    },

    async completeMfa(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.mfaComplete, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const methodValue = text(input.method);
      const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue)
        ? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp")
        : "totp";
      const result = await engine.completeMfa({
        mfaToken: text(input.mfaToken),
        method,
        code: text(input.code),
        challengeId: text(input.challengeId) || undefined,
        session: {
          ip: ctx.ip,
          userAgent: ctx.req.headers.get("user-agent") ?? undefined,
        },
      });
      if (result.code === "mfa-required") return json(result, 401);
      if (!result.ok || !result.session || !result.user) return json(result, 400);
      establishAuthSession(ctx, result.session, result.user);
      if (onSignedIn) {
        return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin));
      }
      return json(result);
    },

    async startImpersonation(ctx: Context): Promise<Response> {
      const actor = getAuthUser(ctx);
      const current = getAuthSession(ctx);
      if (!actor) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.impersonationStart, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const result = await engine.startImpersonation(actor.id, text(input.targetUserId), {
        reason: text(input.reason) || undefined,
        sessionId: current?.id,
        ip: ctx.ip,
        userAgent: ctx.req.headers.get("user-agent") ?? undefined,
      });
      if (!result.ok || !result.session || !result.user) return json(result, 403);
      establishAuthSession(ctx, result.session, result.user);
      return json(result);
    },

    async stopImpersonation(ctx: Context): Promise<Response> {
      const current = getAuthSession(ctx);
      if (!current) return json({ ok: false, error: "Unauthorized" }, 401);
      const result = await engine.stopImpersonation(current.id);
      if (result.code === "mfa-required") return json(result, 401);
      if (!result.ok || !result.session || !result.user) return json(result, 400);
      establishAuthSession(ctx, result.session, result.user);
      return json(result);
    },

    async sessions(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      return json({
        ok: true,
        sessions: await engine.listSessions(user.id),
        currentSessionId: getAuthSession(ctx)?.id,
      });
    },

    async revokeSession(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.sessionRevoke, ctx.req);
      if (!validation.ok) return validation.response;
      return json({
        ok: await engine.revokeSession(user.id, text(validation.value.sessionId)),
      });
    },

    async passkeyRegistrationOptions(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.passkeyRegistrationOptions, ctx.req);
      if (!validation.ok) return validation.response;
      const config = passkeyConfig(ctx);
      return json({
        ok: true,
        ...(await engine.beginPasskeyRegistration(user.id, config)),
      });
    },

    async passkeyRegistrationVerify(ctx: Context): Promise<Response> {
      const user = getAuthUser(ctx);
      if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
      const validation = await parseBody(schemas.passkeyRegistrationVerify, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const ok = await engine.finishPasskeyRegistration(user.id, {
        key: text(input.key),
        response: input.response,
        name: text(input.name) || undefined,
      });
      return json({ ok }, ok ? 200 : 400);
    },

    async passkeyAuthenticationOptions(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.passkeyAuthenticationOptions, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const config = passkeyConfig(ctx);
      return json({
        ok: true,
        ...(await engine.beginPasskeyAuthentication({
          identifier: text(input.identifier) || undefined,
          rpId: config.rpId,
          origin: config.origin,
        })),
      });
    },

    async passkeyAuthenticationVerify(ctx: Context): Promise<Response> {
      const validation = await parseBody(schemas.passkeyAuthenticationVerify, ctx.req);
      if (!validation.ok) return validation.response;
      const input = validation.value;
      const result = await engine.finishPasskeyAuthentication({
        key: text(input.key),
        response: input.response,
        session: {
          ip: ctx.ip,
          userAgent: ctx.req.headers.get("user-agent") ?? undefined,
        },
      });
      if (result.code === "mfa-required") return json(result, 401);
      if (!result.ok || !result.session || !result.user) return json(result, 400);
      establishAuthSession(ctx, result.session, result.user);
      if (onSignedIn) return onSignedIn(ctx);
      return json(result);
    },
  };
}
