import type { Db, Row } from "@wrnexus/db";
import type { AuthStore } from "../store.ts";
import { inferIdentityType, normalizeIdentity } from "../normalize.ts";
import type {
  AuthIdentity,
  AuthSecurityEvent,
  AuthSession,
  AuthUser,
  LoginAttempt,
  OAuthAccount,
  OneTimeToken,
  OtpChallenge,
  PasskeyCredential,
  PasswordCredential,
  RecoveryCodeRecord,
  TotpCredential,
  TrustedDevice,
} from "../types.ts";

function bool(value: unknown): boolean {
  return value === true || value === 1 || value === "1";
}
function json<T>(value: unknown, fallback: T): T {
  if (typeof value !== "string" || !value) return fallback;
  try {
    return JSON.parse(value) as T;
  } catch {
    return fallback;
  }
}
function placeholders(sql: string, dialect: string): string {
  if (dialect !== "postgres") return sql;
  let index = 0;
  return sql.replace(/\?/g, () => `$${++index}`);
}

export class SqlAuthStore implements AuthStore {
  constructor(private readonly db: Db) {}
  private sql(value: string): string {
    return placeholders(value, this.db.driver.dialect);
  }
  private async one(sql: string, params: unknown[] = []): Promise<Row | undefined> {
    return (await this.db.one(this.sql(sql), params)) ?? undefined;
  }
  private async all(sql: string, params: unknown[] = []): Promise<Row[]> {
    return this.db.all(this.sql(sql), params);
  }
  private exec(sql: string, params: unknown[] = []) {
    return this.db.exec(this.sql(sql), params);
  }

  async createUser(user: AuthUser): Promise<void> {
    await this.exec(
      "INSERT INTO wrn_auth_users (id,username,display_name,avatar_url,status,roles_json,email_verified,phone_verified,mfa_enabled,locale,timezone,created_at,updated_at,last_login_at,metadata_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
      [
        user.id,
        user.username,
        user.displayName,
        user.avatarUrl,
        user.status,
        JSON.stringify(user.roles),
        Number(user.emailVerified),
        Number(user.phoneVerified),
        Number(user.mfaEnabled),
        user.locale,
        user.timezone,
        user.createdAt,
        user.updatedAt,
        user.lastLoginAt,
        JSON.stringify(user.metadata ?? {}),
      ],
    );
  }
  async updateUser(user: AuthUser): Promise<void> {
    await this.exec(
      "UPDATE wrn_auth_users SET username=?,display_name=?,avatar_url=?,status=?,roles_json=?,email_verified=?,phone_verified=?,mfa_enabled=?,locale=?,timezone=?,updated_at=?,last_login_at=?,metadata_json=? WHERE id=?",
      [
        user.username,
        user.displayName,
        user.avatarUrl,
        user.status,
        JSON.stringify(user.roles),
        Number(user.emailVerified),
        Number(user.phoneVerified),
        Number(user.mfaEnabled),
        user.locale,
        user.timezone,
        user.updatedAt,
        user.lastLoginAt,
        JSON.stringify(user.metadata ?? {}),
        user.id,
      ],
    );
  }
  private user(row: Row | undefined): AuthUser | undefined {
    if (!row) return undefined;
    return {
      id: String(row.id),
      username: row.username ? String(row.username) : undefined,
      displayName: row.display_name ? String(row.display_name) : undefined,
      avatarUrl: row.avatar_url ? String(row.avatar_url) : undefined,
      status: String(row.status) as AuthUser["status"],
      roles: json(row.roles_json, []),
      emailVerified: bool(row.email_verified),
      phoneVerified: bool(row.phone_verified),
      mfaEnabled: bool(row.mfa_enabled),
      locale: row.locale ? String(row.locale) : undefined,
      timezone: row.timezone ? String(row.timezone) : undefined,
      createdAt: Number(row.created_at),
      updatedAt: Number(row.updated_at),
      lastLoginAt: row.last_login_at == null ? undefined : Number(row.last_login_at),
      metadata: json(row.metadata_json, {}),
    };
  }
  async findUserById(id: string): Promise<AuthUser | undefined> {
    return this.user(await this.one("SELECT * FROM wrn_auth_users WHERE id=?", [id]));
  }
  async listUsers(): Promise<AuthUser[]> {
    return (await this.all("SELECT * FROM wrn_auth_users ORDER BY created_at")).map((row) =>
      this.user(row)!,
    );
  }

  async createIdentity(x: AuthIdentity): Promise<void> {
    await this.exec(
      "INSERT INTO wrn_auth_identities (id,user_id,type,value,normalized_value,is_primary,verified_at,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.type,
        x.value,
        x.normalizedValue,
        Number(x.primary),
        x.verifiedAt,
        x.createdAt,
        x.updatedAt,
      ],
    );
  }
  async updateIdentity(x: AuthIdentity): Promise<void> {
    await this.exec(
      "UPDATE wrn_auth_identities SET value=?,normalized_value=?,is_primary=?,verified_at=?,updated_at=? WHERE id=?",
      [x.value, x.normalizedValue, Number(x.primary), x.verifiedAt, x.updatedAt, x.id],
    );
  }
  private identity(row: Row | undefined): AuthIdentity | undefined {
    if (!row) return;
    return {
      id: String(row.id),
      userId: String(row.user_id),
      type: String(row.type) as AuthIdentity["type"],
      value: String(row.value),
      normalizedValue: String(row.normalized_value),
      primary: bool(row.is_primary),
      verifiedAt: row.verified_at == null ? undefined : Number(row.verified_at),
      createdAt: Number(row.created_at),
      updatedAt: Number(row.updated_at),
    };
  }
  async findIdentity(type: AuthIdentity["type"], value: string) {
    return this.identity(
      await this.one("SELECT * FROM wrn_auth_identities WHERE type=? AND normalized_value=?", [
        type,
        value,
      ]),
    );
  }
  async listIdentities(userId: string) {
    return (
      await this.all(
        "SELECT * FROM wrn_auth_identities WHERE user_id=? ORDER BY is_primary DESC, created_at",
        [userId],
      )
    ).map((r) => this.identity(r)!);
  }

  async setPassword(x: PasswordCredential) {
    const current = await this.getPassword(x.userId);
    if (current)
      await this.exec(
        "UPDATE wrn_auth_password_credentials SET password_hash=?,password_version=?,changed_at=?,must_change=? WHERE user_id=?",
        [x.passwordHash, x.passwordVersion, x.changedAt, Number(x.mustChange), x.userId],
      );
    else
      await this.exec(
        "INSERT INTO wrn_auth_password_credentials (user_id,password_hash,password_version,changed_at,must_change) VALUES (?,?,?,?,?)",
        [x.userId, x.passwordHash, x.passwordVersion, x.changedAt, Number(x.mustChange)],
      );
  }
  async getPassword(userId: string) {
    const r = await this.one("SELECT * FROM wrn_auth_password_credentials WHERE user_id=?", [
      userId,
    ]);
    return r
      ? {
          userId: String(r.user_id),
          passwordHash: String(r.password_hash),
          passwordVersion: Number(r.password_version),
          changedAt: Number(r.changed_at),
          mustChange: bool(r.must_change),
        }
      : undefined;
  }

  async createSession(x: AuthSession) {
    await this.exec(
      "INSERT INTO wrn_auth_sessions (id,user_id,device_id,created_at,last_seen_at,expires_at,absolute_expires_at,ip,user_agent,trusted,revoked_at,revoke_reason,metadata_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.deviceId,
        x.createdAt,
        x.lastSeenAt,
        x.expiresAt,
        x.absoluteExpiresAt,
        x.ip,
        x.userAgent,
        Number(x.trusted),
        x.revokedAt,
        x.revokeReason,
        JSON.stringify(x.metadata ?? {}),
      ],
    );
  }
  async updateSession(x: AuthSession) {
    await this.exec(
      "UPDATE wrn_auth_sessions SET last_seen_at=?,expires_at=?,trusted=?,revoked_at=?,revoke_reason=?,metadata_json=? WHERE id=?",
      [
        x.lastSeenAt,
        x.expiresAt,
        Number(x.trusted),
        x.revokedAt,
        x.revokeReason,
        JSON.stringify(x.metadata ?? {}),
        x.id,
      ],
    );
  }
  private session(r: Row | undefined): AuthSession | undefined {
    if (!r) return;
    return {
      id: String(r.id),
      userId: String(r.user_id),
      deviceId: String(r.device_id),
      createdAt: Number(r.created_at),
      lastSeenAt: Number(r.last_seen_at),
      expiresAt: Number(r.expires_at),
      absoluteExpiresAt: Number(r.absolute_expires_at),
      ip: r.ip ? String(r.ip) : undefined,
      userAgent: r.user_agent ? String(r.user_agent) : undefined,
      trusted: bool(r.trusted),
      revokedAt: r.revoked_at == null ? undefined : Number(r.revoked_at),
      revokeReason: r.revoke_reason ? String(r.revoke_reason) : undefined,
      metadata: json(r.metadata_json, {}),
    };
  }
  async findSession(id: string) {
    return this.session(await this.one("SELECT * FROM wrn_auth_sessions WHERE id=?", [id]));
  }
  async listSessions(userId: string) {
    return (
      await this.all("SELECT * FROM wrn_auth_sessions WHERE user_id=? ORDER BY last_seen_at DESC", [
        userId,
      ])
    ).map((r) => this.session(r)!);
  }
  async deleteSession(id: string) {
    await this.exec("DELETE FROM wrn_auth_sessions WHERE id=?", [id]);
  }

  async createTrustedDevice(x: TrustedDevice) {
    await this.exec(
      "INSERT INTO wrn_auth_trusted_devices (id,user_id,name,fingerprint_hash,created_at,last_seen_at,expires_at,revoked_at) VALUES (?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.name,
        x.fingerprintHash,
        x.createdAt,
        x.lastSeenAt,
        x.expiresAt,
        x.revokedAt,
      ],
    );
  }
  async updateTrustedDevice(x: TrustedDevice) {
    await this.exec(
      "UPDATE wrn_auth_trusted_devices SET name=?,last_seen_at=?,expires_at=?,revoked_at=? WHERE id=?",
      [x.name, x.lastSeenAt, x.expiresAt, x.revokedAt, x.id],
    );
  }
  private device(r: Row | undefined): TrustedDevice | undefined {
    if (!r) return;
    return {
      id: String(r.id),
      userId: String(r.user_id),
      name: String(r.name),
      fingerprintHash: String(r.fingerprint_hash),
      createdAt: Number(r.created_at),
      lastSeenAt: Number(r.last_seen_at),
      expiresAt: Number(r.expires_at),
      revokedAt: r.revoked_at == null ? undefined : Number(r.revoked_at),
    };
  }
  async findTrustedDeviceByFingerprint(userId: string, hash: string) {
    return this.device(
      await this.one(
        "SELECT * FROM wrn_auth_trusted_devices WHERE user_id=? AND fingerprint_hash=?",
        [userId, hash],
      ),
    );
  }
  async listTrustedDevices(userId: string) {
    return (
      await this.all(
        "SELECT * FROM wrn_auth_trusted_devices WHERE user_id=? ORDER BY last_seen_at DESC",
        [userId],
      )
    ).map((r) => this.device(r)!);
  }

  async createToken(x: OneTimeToken) {
    await this.exec(
      "INSERT INTO wrn_auth_tokens (id,user_id,purpose,token_hash,target,created_at,expires_at,used_at,attempts,max_attempts,metadata_json) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.purpose,
        x.tokenHash,
        x.target,
        x.createdAt,
        x.expiresAt,
        x.usedAt,
        x.attempts,
        x.maxAttempts,
        JSON.stringify(x.metadata ?? {}),
      ],
    );
  }
  async updateToken(x: OneTimeToken) {
    await this.exec("UPDATE wrn_auth_tokens SET used_at=?,attempts=?,metadata_json=? WHERE id=?", [
      x.usedAt,
      x.attempts,
      JSON.stringify(x.metadata ?? {}),
      x.id,
    ]);
  }
  private token(r: Row | undefined): OneTimeToken | undefined {
    if (!r) return;
    return {
      id: String(r.id),
      userId: String(r.user_id),
      purpose: String(r.purpose) as OneTimeToken["purpose"],
      tokenHash: String(r.token_hash),
      target: r.target ? String(r.target) : undefined,
      createdAt: Number(r.created_at),
      expiresAt: Number(r.expires_at),
      usedAt: r.used_at == null ? undefined : Number(r.used_at),
      attempts: Number(r.attempts),
      maxAttempts: Number(r.max_attempts),
      metadata: json(r.metadata_json, {}),
    };
  }
  async findTokenByHash(hash: string) {
    return this.token(await this.one("SELECT * FROM wrn_auth_tokens WHERE token_hash=?", [hash]));
  }

  async createOtp(x: OtpChallenge) {
    await this.exec(
      "INSERT INTO wrn_auth_otp_challenges (id,user_id,method,purpose,destination,code_hash,created_at,expires_at,used_at,attempts,max_attempts) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.method,
        x.purpose,
        x.destination,
        x.codeHash,
        x.createdAt,
        x.expiresAt,
        x.usedAt,
        x.attempts,
        x.maxAttempts,
      ],
    );
  }
  async updateOtp(x: OtpChallenge) {
    await this.exec("UPDATE wrn_auth_otp_challenges SET used_at=?,attempts=? WHERE id=?", [
      x.usedAt,
      x.attempts,
      x.id,
    ]);
  }
  async findOtp(id: string) {
    const r = await this.one("SELECT * FROM wrn_auth_otp_challenges WHERE id=?", [id]);
    return r
      ? {
          id: String(r.id),
          userId: String(r.user_id),
          method: String(r.method) as OtpChallenge["method"],
          purpose: String(r.purpose ?? "verification") as OtpChallenge["purpose"],
          destination: String(r.destination),
          codeHash: String(r.code_hash),
          createdAt: Number(r.created_at),
          expiresAt: Number(r.expires_at),
          usedAt: r.used_at == null ? undefined : Number(r.used_at),
          attempts: Number(r.attempts),
          maxAttempts: Number(r.max_attempts),
        }
      : undefined;
  }

  async createTotp(x: TotpCredential) {
    await this.exec(
      "INSERT INTO wrn_auth_totp_credentials (id,user_id,label,secret,created_at,verified_at,last_counter) VALUES (?,?,?,?,?,?,?)",
      [x.id, x.userId, x.label, x.secret, x.createdAt, x.verifiedAt, x.lastCounter],
    );
  }
  async updateTotp(x: TotpCredential) {
    await this.exec(
      "UPDATE wrn_auth_totp_credentials SET label=?,verified_at=?,last_counter=? WHERE id=?",
      [x.label, x.verifiedAt, x.lastCounter, x.id],
    );
  }
  private totpRow(r: Row): TotpCredential {
    return {
      id: String(r.id),
      userId: String(r.user_id),
      label: String(r.label),
      secret: String(r.secret),
      createdAt: Number(r.created_at),
      verifiedAt: r.verified_at == null ? undefined : Number(r.verified_at),
      lastCounter: r.last_counter == null ? undefined : Number(r.last_counter),
    };
  }
  async listTotp(userId: string) {
    return (
      await this.all("SELECT * FROM wrn_auth_totp_credentials WHERE user_id=?", [userId])
    ).map((r) => this.totpRow(r));
  }
  async deleteTotp(id: string) {
    await this.exec("DELETE FROM wrn_auth_totp_credentials WHERE id=?", [id]);
  }

  async createRecoveryCodes(codes: RecoveryCodeRecord[]) {
    for (const x of codes)
      await this.exec(
        "INSERT INTO wrn_auth_recovery_codes (id,user_id,code_hash,created_at,used_at) VALUES (?,?,?,?,?)",
        [x.id, x.userId, x.codeHash, x.createdAt, x.usedAt],
      );
  }
  async updateRecoveryCode(x: RecoveryCodeRecord) {
    await this.exec("UPDATE wrn_auth_recovery_codes SET used_at=? WHERE id=?", [x.usedAt, x.id]);
  }
  async listRecoveryCodes(userId: string) {
    return (await this.all("SELECT * FROM wrn_auth_recovery_codes WHERE user_id=?", [userId])).map(
      (r) => ({
        id: String(r.id),
        userId: String(r.user_id),
        codeHash: String(r.code_hash),
        createdAt: Number(r.created_at),
        usedAt: r.used_at == null ? undefined : Number(r.used_at),
      }),
    );
  }
  async deleteRecoveryCodes(userId: string) {
    await this.exec("DELETE FROM wrn_auth_recovery_codes WHERE user_id=?", [userId]);
  }

  async createPasskey(x: PasskeyCredential) {
    await this.exec(
      "INSERT INTO wrn_auth_passkeys (id,user_id,credential_id,public_key,counter,transports_json,name,created_at,last_used_at,backed_up,device_type) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.credentialId,
        x.publicKey,
        x.counter,
        JSON.stringify(x.transports),
        x.name,
        x.createdAt,
        x.lastUsedAt,
        x.backedUp == null ? undefined : Number(x.backedUp),
        x.deviceType,
      ],
    );
  }
  async updatePasskey(x: PasskeyCredential) {
    await this.exec(
      "UPDATE wrn_auth_passkeys SET counter=?,transports_json=?,name=?,last_used_at=?,backed_up=?,device_type=? WHERE id=?",
      [
        x.counter,
        JSON.stringify(x.transports),
        x.name,
        x.lastUsedAt,
        x.backedUp == null ? undefined : Number(x.backedUp),
        x.deviceType,
        x.id,
      ],
    );
  }
  private passkey(r: Row | undefined): PasskeyCredential | undefined {
    if (!r) return;
    return {
      id: String(r.id),
      userId: String(r.user_id),
      credentialId: String(r.credential_id),
      publicKey: String(r.public_key),
      counter: Number(r.counter),
      transports: json(r.transports_json, []),
      name: String(r.name),
      createdAt: Number(r.created_at),
      lastUsedAt: r.last_used_at == null ? undefined : Number(r.last_used_at),
      backedUp: r.backed_up == null ? undefined : bool(r.backed_up),
      deviceType: r.device_type ? String(r.device_type) : undefined,
    };
  }
  async findPasskeyByCredentialId(id: string) {
    return this.passkey(
      await this.one("SELECT * FROM wrn_auth_passkeys WHERE credential_id=?", [id]),
    );
  }
  async listPasskeys(userId: string) {
    return (await this.all("SELECT * FROM wrn_auth_passkeys WHERE user_id=?", [userId])).map((r) =>
      this.passkey(r)!,
    );
  }
  async deletePasskey(id: string) {
    await this.exec("DELETE FROM wrn_auth_passkeys WHERE id=?", [id]);
  }

  async createOAuthAccount(x: OAuthAccount) {
    await this.exec(
      "INSERT INTO wrn_auth_oauth_accounts (id,user_id,provider,provider_account_id,email,access_token,refresh_token,token_expires_at,scope,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.provider,
        x.providerAccountId,
        x.email,
        x.accessToken,
        x.refreshToken,
        x.tokenExpiresAt,
        x.scope,
        x.createdAt,
        x.updatedAt,
      ],
    );
  }
  async updateOAuthAccount(x: OAuthAccount) {
    await this.exec(
      "UPDATE wrn_auth_oauth_accounts SET email=?,access_token=?,refresh_token=?,token_expires_at=?,scope=?,updated_at=? WHERE id=?",
      [x.email, x.accessToken, x.refreshToken, x.tokenExpiresAt, x.scope, x.updatedAt, x.id],
    );
  }
  private oauth(r: Row | undefined): OAuthAccount | undefined {
    if (!r) return;
    return {
      id: String(r.id),
      userId: String(r.user_id),
      provider: String(r.provider),
      providerAccountId: String(r.provider_account_id),
      email: r.email ? String(r.email) : undefined,
      accessToken: r.access_token ? String(r.access_token) : undefined,
      refreshToken: r.refresh_token ? String(r.refresh_token) : undefined,
      tokenExpiresAt: r.token_expires_at == null ? undefined : Number(r.token_expires_at),
      scope: r.scope ? String(r.scope) : undefined,
      createdAt: Number(r.created_at),
      updatedAt: Number(r.updated_at),
    };
  }
  async findOAuthAccount(provider: string, id: string) {
    return this.oauth(
      await this.one(
        "SELECT * FROM wrn_auth_oauth_accounts WHERE provider=? AND provider_account_id=?",
        [provider, id],
      ),
    );
  }
  async listOAuthAccounts(userId: string) {
    return (await this.all("SELECT * FROM wrn_auth_oauth_accounts WHERE user_id=?", [userId])).map(
      (r) => this.oauth(r)!,
    );
  }
  async deleteOAuthAccount(id: string) {
    await this.exec("DELETE FROM wrn_auth_oauth_accounts WHERE id=?", [id]);
  }

  async createLoginAttempt(x: LoginAttempt) {
    const identifier = x.identifier
      ? normalizeIdentity(inferIdentityType(x.identifier), x.identifier)
      : undefined;
    await this.exec(
      "INSERT INTO wrn_auth_login_attempts (id,identifier,user_id,success,reason,ip,user_agent,created_at,risk_score,risk_level) VALUES (?,?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        identifier,
        x.userId,
        Number(x.success),
        x.reason,
        x.ip,
        x.userAgent,
        x.createdAt,
        x.riskScore,
        x.riskLevel,
      ],
    );
  }
  async listRecentLoginAttempts(identifier: string, since: number) {
    const normalized = normalizeIdentity(inferIdentityType(identifier), identifier);
    const rows = await this.all(
      "SELECT * FROM wrn_auth_login_attempts WHERE identifier=? AND created_at>=? ORDER BY created_at DESC",
      [normalized, since],
    );
    return rows.map((r) => ({
      id: String(r.id),
      identifier: r.identifier ? String(r.identifier) : undefined,
      userId: r.user_id ? String(r.user_id) : undefined,
      success: bool(r.success),
      reason: r.reason ? String(r.reason) : undefined,
      ip: r.ip ? String(r.ip) : undefined,
      userAgent: r.user_agent ? String(r.user_agent) : undefined,
      createdAt: Number(r.created_at),
      riskScore: Number(r.risk_score),
      riskLevel: String(r.risk_level) as LoginAttempt["riskLevel"],
    }));
  }

  async createSecurityEvent(x: AuthSecurityEvent) {
    await this.exec(
      "INSERT INTO wrn_auth_security_events (id,user_id,type,severity,actor_user_id,session_id,ip,user_agent,created_at,data_json) VALUES (?,?,?,?,?,?,?,?,?,?)",
      [
        x.id,
        x.userId,
        x.type,
        x.severity,
        x.actorUserId,
        x.sessionId,
        x.ip,
        x.userAgent,
        x.createdAt,
        JSON.stringify(x.data ?? {}),
      ],
    );
  }
  async listSecurityEvents(userId: string, limit = 100) {
    const rows = await this.all(
      "SELECT * FROM wrn_auth_security_events WHERE user_id=? ORDER BY created_at DESC LIMIT ?",
      [userId, limit],
    );
    return rows.map((r) => ({
      id: String(r.id),
      userId: r.user_id ? String(r.user_id) : undefined,
      type: String(r.type),
      severity: String(r.severity) as AuthSecurityEvent["severity"],
      actorUserId: r.actor_user_id ? String(r.actor_user_id) : undefined,
      sessionId: r.session_id ? String(r.session_id) : undefined,
      ip: r.ip ? String(r.ip) : undefined,
      userAgent: r.user_agent ? String(r.user_agent) : undefined,
      createdAt: Number(r.created_at),
      data: json(r.data_json, {}),
    }));
  }
}
