import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import type { MessageRevision } from '../../../../shared-kernel';
import { ServerDirectoryFacade } from '../../../server-directory';

const STORAGE_KEY = 'metoyou.messageSigningKeyPair';

interface StoredSigningKeyPair {
  publicKeyJwk: JsonWebKey;
  privateKeyJwk: JsonWebKey;
}

@Injectable({ providedIn: 'root' })
export class MessageSigningService {
  private readonly http = inject(HttpClient);
  private readonly serverDirectory = inject(ServerDirectoryFacade);
  private keyPairPromise: Promise<CryptoKeyPair> | null = null;

  async ensureSigningKeyPair(): Promise<CryptoKeyPair> {
    if (!this.keyPairPromise) {
      this.keyPairPromise = this.loadOrCreateKeyPair();
    }

    return await this.keyPairPromise;
  }

  async getPublicKeyJwk(): Promise<JsonWebKey> {
    const keyPair = await this.ensureSigningKeyPair();

    return await crypto.subtle.exportKey('jwk', keyPair.publicKey);
  }

  async registerSigningPublicKeyIfNeeded(): Promise<void> {
    const activeServer = this.serverDirectory.activeServer();

    if (!activeServer) {
      return;
    }

    const publicKeyJwk = await this.getPublicKeyJwk();
    const apiBase = `${activeServer.url}/api`;

    await firstValueFrom(this.http.put(`${apiBase}/users/me/signing-key`, { publicKeyJwk }));
  }

  async signRevision(revision: MessageRevision): Promise<string> {
    const keyPair = await this.ensureSigningKeyPair();
    const payload = this.canonicalRevisionPayload(revision);
    const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload));
    const signature = await crypto.subtle.sign('Ed25519', keyPair.privateKey, digest);

    return this.bufferToBase64Url(signature);
  }

  async fetchSigningPublicKey(userId: string): Promise<JsonWebKey | null> {
    const activeServer = this.serverDirectory.activeServer();

    if (!activeServer) {
      return null;
    }

    try {
      const response = await firstValueFrom(
        this.http.get<{ publicKeyJwk: JsonWebKey }>(`${activeServer.url}/api/users/${encodeURIComponent(userId)}/signing-public-key`)
      );

      return response.publicKeyJwk;
    } catch {
      return null;
    }
  }

  async verifyRevisionSignature(
    revision: MessageRevision,
    publicKeyJwk: JsonWebKey
  ): Promise<boolean> {
    if (!revision.signature) {
      return false;
    }

    try {
      const publicKey = await crypto.subtle.importKey(
        'jwk',
        publicKeyJwk,
        { name: 'Ed25519' },
        false,
        ['verify']
      );
      const payload = this.canonicalRevisionPayload(revision);
      const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload));
      const signature = this.base64UrlToBuffer(revision.signature);

      return await crypto.subtle.verify('Ed25519', publicKey, signature, digest);
    } catch {
      return false;
    }
  }

  private async loadOrCreateKeyPair(): Promise<CryptoKeyPair> {
    const stored = this.readStoredKeyPair();

    if (stored) {
      const [publicKey, privateKey] = await Promise.all([crypto.subtle.importKey('jwk', stored.publicKeyJwk, { name: 'Ed25519' }, true, ['verify']), crypto.subtle.importKey('jwk', stored.privateKeyJwk, { name: 'Ed25519' }, false, ['sign'])]);

      return { publicKey, privateKey };
    }

    const generated = await crypto.subtle.generateKey(
      { name: 'Ed25519' },
      true,
      ['sign', 'verify']
    );
    const [publicKeyJwk, privateKeyJwk] = await Promise.all([crypto.subtle.exportKey('jwk', generated.publicKey), crypto.subtle.exportKey('jwk', generated.privateKey)]);

    this.writeStoredKeyPair({ publicKeyJwk, privateKeyJwk });

    return generated;
  }

  private canonicalRevisionPayload(revision: MessageRevision): string {
    const { signature: _signature, ...unsigned } = revision;

    return JSON.stringify(unsigned, Object.keys(unsigned).sort());
  }

  private readStoredKeyPair(): StoredSigningKeyPair | null {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);

      if (!raw) {
        return null;
      }

      return JSON.parse(raw) as StoredSigningKeyPair;
    } catch {
      return null;
    }
  }

  private writeStoredKeyPair(pair: StoredSigningKeyPair): void {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(pair));
  }

  private bufferToBase64Url(buffer: ArrayBuffer): string {
    const bytes = new Uint8Array(buffer);

    return btoa(String.fromCharCode(...bytes))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/g, '');
  }

  private base64UrlToBuffer(value: string): ArrayBuffer {
    const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
    const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
    const binary = atob(padded);
    const bytes = new Uint8Array(binary.length);

    for (let index = 0; index < binary.length; index += 1) {
      bytes[index] = binary.charCodeAt(index);
    }

    return bytes.buffer;
  }
}
