import * as plugins from '../../plugins.js';
import { logger } from '../../logging.js';
import type { IUser } from '../../types.js';
import type { OpsServer } from '../classes.opsserver.js';
import { PasskeyManager } from '../classes.passkeymanager.js';
import * as interfaces from '../../../ts_interfaces/index.js';

export interface IJwtData {
  userId: string;
  username: string;
  role: 'admin' | 'user';
  status: 'loggedIn' | 'loggedOut';
  authMethod: 'passkey';
  credentialId: string;
  expiresAt: number;
}

export class AdminHandler {
  public typedrouter = new plugins.typedrequest.TypedRouter();
  public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
  public passkeyManager: PasskeyManager;

  constructor(
    private opsServerRef: OpsServer,
    optionsArg: { developmentOrigin?: string } = {},
  ) {
    this.passkeyManager = new PasskeyManager(opsServerRef, optionsArg);
    this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
  }

  public async initialize(): Promise<void> {
    this.smartjwtInstance = new plugins.smartjwt.SmartJwt<IJwtData>();
    await this.smartjwtInstance.init();
    await this.passkeyManager.initialize();
    this.registerHandlers();
  }

  private async createIdentityForUser(
    user: Pick<IUser, 'id' | 'username' | 'role'>,
    credentialId: string,
    expiresAt: number,
  ): Promise<interfaces.data.IIdentity> {
    const userId = String(user.id || user.username);
    const jwt = await this.smartjwtInstance.createJWT({
      userId,
      username: user.username,
      role: user.role,
      status: 'loggedIn',
      authMethod: 'passkey',
      credentialId,
      expiresAt,
    });

    return {
      jwt,
      userId,
      username: user.username,
      expiresAt,
      role: user.role,
    };
  }

  private authError(
    codeArg: interfaces.data.TRpcErrorCode,
    messageArg: string,
  ): InstanceType<typeof plugins.typedrequest.TypedResponseError> {
    return new plugins.typedrequest.TypedResponseError(messageArg, {
      code: codeArg,
      retryable: false,
    } satisfies interfaces.data.IRpcErrorData);
  }

  private isAuthError(errorArg: unknown): boolean {
    if (!errorArg || typeof errorArg !== 'object') return false;
    const errorData = (errorArg as { errorData?: unknown }).errorData;
    if (!errorData || typeof errorData !== 'object') return false;
    const code = (errorData as { code?: unknown }).code;
    return [
      'AUTH_REQUIRED',
      'AUTH_INVALID',
      'AUTH_EXPIRED',
    ].includes(String(code));
  }

  public async getVerifiedIdentity(
    identityArg: interfaces.data.IIdentity | null | undefined,
  ): Promise<interfaces.data.IIdentity> {
    if (!identityArg?.jwt) {
      throw this.authError('AUTH_REQUIRED', 'No identity provided');
    }

    let jwtData: IJwtData;
    try {
      jwtData = await this.smartjwtInstance.verifyJWTAndGetData(identityArg.jwt);
    } catch {
      throw this.authError('AUTH_INVALID', 'Valid identity required');
    }

    if (
      jwtData.expiresAt < Date.now()
    ) {
      throw this.authError('AUTH_EXPIRED', 'Identity has expired');
    }

    if (
      jwtData.status !== 'loggedIn'
      || jwtData.authMethod !== 'passkey'
      || !jwtData.credentialId
    ) {
      throw this.authError('AUTH_INVALID', 'Valid passkey identity required');
    }

    const user = await this.opsServerRef.oneboxRef.database.getUserByUsername(jwtData.username);
    if (!user) {
      throw this.authError('AUTH_INVALID', 'Valid identity required');
    }

    const userId = String(user.id || user.username);
    if (
      jwtData.userId !== userId
      || !(await this.passkeyManager.isCredentialActive(user.username, jwtData.credentialId))
    ) {
      throw this.authError('AUTH_INVALID', 'Valid passkey identity required');
    }

    return {
      jwt: identityArg.jwt,
      userId,
      username: user.username,
      expiresAt: jwtData.expiresAt,
      role: user.role,
    };
  }

  public async getVerifiedAdminIdentity(
    identityArg: interfaces.data.IIdentity | null | undefined,
  ): Promise<interfaces.data.IIdentity> {
    const identity = await this.getVerifiedIdentity(identityArg);
    if (identity.role !== 'admin') {
      throw this.authError('FORBIDDEN', 'Admin access required');
    }
    return identity;
  }

  private registerHandlers(): void {
    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetAdminAuthState>(
        'getAdminAuthState',
        async (dataArg) => {
          try {
            return await this.passkeyManager.getAuthState(dataArg.origin);
          } catch (error) {
            logger.warn(`Unable to read passkey authentication state: ${this.errorMessage(error)}`);
            throw new plugins.typedrequest.TypedResponseError(
              'Passkey authentication state is unavailable',
            );
          }
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_BeginAdminPasskeyRegistration>(
        'beginAdminPasskeyRegistration',
        async (dataArg) => {
          return await this.runPasskeyCeremony(() =>
            this.passkeyManager.beginRegistration(dataArg.origin, dataArg.bootstrapToken)
          );
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_FinishAdminPasskeyRegistration>(
        'finishAdminPasskeyRegistration',
        async (dataArg) => {
          return await this.runPasskeyCeremony(async () => {
            const result = await this.passkeyManager.finishRegistration(
              dataArg.ceremonyId,
              dataArg.bootstrapToken,
              dataArg.response,
            );
            const identity = await this.createIdentityForUser(
              result.user,
              result.credentialId,
              Date.now() + 24 * 3600 * 1000,
            );
            logger.info(`Administrator enrolled a Onebox passkey: ${result.user.username}`);
            return { identity };
          });
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_BeginAdminPasskeyAuthentication>(
        'beginAdminPasskeyAuthentication',
        async (dataArg) => {
          return await this.runPasskeyCeremony(() =>
            this.passkeyManager.beginAuthentication(dataArg.origin)
          );
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_FinishAdminPasskeyAuthentication>(
        'finishAdminPasskeyAuthentication',
        async (dataArg) => {
          return await this.runPasskeyCeremony(async () => {
            const result = await this.passkeyManager.finishAuthentication(
              dataArg.ceremonyId,
              dataArg.response,
            );
            const identity = await this.createIdentityForUser(
              result.user,
              result.credentialId,
              Date.now() + 24 * 3600 * 1000,
            );
            logger.info(`Administrator signed in with a passkey: ${result.user.username}`);
            return { identity };
          });
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
        'adminLogout',
        async (_dataArg) => {
          return { ok: true };
        },
      ),
    );

    this.typedrouter.addTypedHandler(
      new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
        'verifyIdentity',
        async (dataArg) => {
          try {
            const identity = await this.getVerifiedIdentity(dataArg.identity);
            return { valid: true, identity };
          } catch (error) {
            if (this.isAuthError(error)) {
              return { valid: false };
            }
            logger.warn(`Unable to verify identity: ${this.errorMessage(error)}`);
            throw new plugins.typedrequest.TypedResponseError(
              'Identity verification is temporarily unavailable',
              {
                code: 'TEMPORARILY_UNAVAILABLE',
                retryable: true,
              } satisfies interfaces.data.IRpcErrorData,
            );
          }
        },
      ),
    );
  }

  private async runPasskeyCeremony<T>(taskArg: () => Promise<T>): Promise<T> {
    try {
      return await taskArg();
    } catch (error) {
      const message = this.errorMessage(error);
      logger.warn(`Passkey ceremony rejected: ${message}`);
      throw new plugins.typedrequest.TypedResponseError(message);
    }
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : 'Passkey ceremony failed';
  }

  public validIdentityGuard = new plugins.smartguard.Guard<{
    identity: interfaces.data.IIdentity;
  }>(
    async (dataArg) => {
      try {
        await this.getVerifiedIdentity(dataArg.identity);
        return true;
      } catch {
        return false;
      }
    },
    { failedHint: 'identity is not valid', name: 'validIdentityGuard' },
  );

  public adminIdentityGuard = new plugins.smartguard.Guard<{
    identity: interfaces.data.IIdentity;
  }>(
    async (dataArg) => {
      try {
        const identity = await this.getVerifiedIdentity(dataArg.identity);
        return identity.role === 'admin';
      } catch {
        return false;
      }
    },
    { failedHint: 'user is not admin', name: 'adminIdentityGuard' },
  );
}
