/**
 * Signature Service
 *
 * Handles message signing and verification for federation protocol.
 */

import { Injectable, Logger, Inject } from "@nestjs/common";
import { createSign, createVerify } from "crypto";
import { FederationService } from "./federation.service";
import type {
  SignableMessage,
  SignatureValidationResult,
  ConnectionRequest,
} from "./types/connection.types";
import type Redis from "ioredis";

@Injectable()
export class SignatureService {
  private readonly logger = new Logger(SignatureService.name);
  private readonly TIMESTAMP_TOLERANCE_MS = 60 * 1000; // 60 seconds
  private readonly CLOCK_SKEW_TOLERANCE_MS = 60 * 1000; // 1 minute for future timestamps
  private readonly NONCE_TTL_SECONDS = 60; // Nonce TTL matches tolerance window

  constructor(
    private readonly federationService: FederationService,
    @Inject("REDIS_CLIENT") private readonly redis: Redis
  ) {}

  /**
   * Sign a message with a private key
   * Returns base64-encoded RSA-SHA256 signature
   */
  sign(message: SignableMessage, privateKey: string): string {
    try {
      // Create canonical JSON representation (sorted keys)
      const canonical = this.canonicalizeMessage(message);

      // Create signature
      const sign = createSign("RSA-SHA256");
      sign.update(canonical);
      sign.end();

      const signature = sign.sign(privateKey, "base64");

      return signature;
    } catch (error) {
      this.logger.error("Failed to sign message", error);
      throw new Error("Failed to sign message");
    }
  }

  /**
   * Verify a message signature with a public key
   */
  verify(
    message: SignableMessage,
    signature: string,
    publicKey: string
  ): SignatureValidationResult {
    try {
      // Create canonical JSON representation (sorted keys)
      const canonical = this.canonicalizeMessage(message);

      // Verify signature
      const verify = createVerify("RSA-SHA256");
      verify.update(canonical);
      verify.end();

      const valid = verify.verify(publicKey, signature, "base64");

      if (!valid) {
        return {
          valid: false,
          error: "Invalid signature",
        };
      }

      return { valid: true };
    } catch (error) {
      this.logger.error("Signature verification failed", error);
      return {
        valid: false,
        error: error instanceof Error ? error.message : "Verification failed",
      };
    }
  }

  /**
   * Validate timestamp is within acceptable range
   * Rejects timestamps older than 5 minutes or more than 1 minute in the future
   */
  validateTimestamp(timestamp: number): boolean {
    const now = Date.now();
    const age = now - timestamp;

    // Reject if too old
    if (age > this.TIMESTAMP_TOLERANCE_MS) {
      this.logger.warn(`Timestamp too old: ${age.toString()}ms`);
      return false;
    }

    // Reject if too far in the future (allow some clock skew)
    if (age < -this.CLOCK_SKEW_TOLERANCE_MS) {
      this.logger.warn(`Timestamp too far in future: ${(-age).toString()}ms`);
      return false;
    }

    return true;
  }

  /**
   * Sign a message using this instance's private key
   */
  async signMessage(message: SignableMessage): Promise<string> {
    const identity = await this.federationService.getInstanceIdentity();

    if (!identity.privateKey) {
      throw new Error("Instance private key not available");
    }

    return this.sign(message, identity.privateKey);
  }

  /**
   * Verify a message signature using a remote instance's public key
   * Fetches the public key from the connection record
   */
  async verifyMessage(
    message: SignableMessage,
    signature: string,
    remoteInstanceId: string
  ): Promise<SignatureValidationResult> {
    try {
      // Fetch remote instance public key from connection record
      // For now, we'll fetch from any connection with this instance
      // In production, this should be cached or fetched from instance identity endpoint
      const connection =
        await this.federationService.getConnectionByRemoteInstanceId(remoteInstanceId);

      if (!connection) {
        return {
          valid: false,
          error: "Remote instance not connected",
        };
      }

      // Verify signature using remote public key
      return this.verify(message, signature, connection.remotePublicKey);
    } catch (error) {
      this.logger.error("Failed to verify message", error);
      return {
        valid: false,
        error: error instanceof Error ? error.message : "Verification failed",
      };
    }
  }

  /**
   * Verify a connection request signature
   */
  async verifyConnectionRequest(request: ConnectionRequest): Promise<SignatureValidationResult> {
    // Extract signature and create message for verification
    const { signature, ...message } = request;

    // Validate timestamp
    if (!this.validateTimestamp(request.timestamp)) {
      return {
        valid: false,
        error: "Request timestamp is outside acceptable range",
      };
    }

    // Check for replay attack (nonce already used)
    const nonceKey = `nonce:${signature}`;
    const nonceExists = await this.redis.get(nonceKey);

    if (nonceExists) {
      this.logger.warn("Replay attack detected: signature already used");
      return {
        valid: false,
        error: "Request rejected: potential replay attack detected",
      };
    }

    // Verify signature using the public key from the request
    const result = this.verify(message, signature, request.publicKey);

    if (!result.valid) {
      const errorMsg = result.error ?? "Unknown error";
      this.logger.warn(`Connection request signature verification failed: ${errorMsg}`);
      return result;
    }

    // Store nonce to prevent replay attacks
    await this.redis.setex(nonceKey, this.NONCE_TTL_SECONDS, "1");

    return result;
  }

  /**
   * Create canonical JSON representation of a message for signing
   * Sorts keys recursively to ensure consistent signatures
   */
  private canonicalizeMessage(message: SignableMessage): string {
    return JSON.stringify(this.sortObjectKeys(message));
  }

  /**
   * Recursively sort object keys for canonical representation
   * @param obj - The object to sort
   * @returns A new object with sorted keys
   */
  private sortObjectKeys(obj: SignableMessage): SignableMessage {
    // Handle arrays - recursively sort elements
    if (Array.isArray(obj)) {
      const sortedArray = obj.map((item: unknown): unknown => {
        if (typeof item === "object" && item !== null) {
          return this.sortObjectKeys(item as SignableMessage);
        }
        return item;
      });
      // Arrays are valid SignableMessage values when nested in objects
      return sortedArray as unknown as SignableMessage;
    }

    // Handle objects - sort keys alphabetically
    const sorted: SignableMessage = {};
    const keys = Object.keys(obj).sort();

    for (const key of keys) {
      const value = obj[key];
      if (typeof value === "object" && value !== null) {
        sorted[key] = this.sortObjectKeys(value as SignableMessage);
      } else {
        sorted[key] = value;
      }
    }

    return sorted;
  }
}
