/**
 * DID Service for SentryAgent.ai AgentIdP.
 * Manages W3C DID Core 1.0 document generation, key management, and AGNTCY agent cards.
 *
 * Key management strategy:
 * - When VAULT_ADDR + VAULT_TOKEN are set: private keys are stored in Vault KV v2 at
 *   `{mount}/data/agentidp/agents/{agentId}/did-key`.
 * - When Vault is not configured (dev mode): `vault_key_path` column stores the marker
 *   `dev:no-vault` and the private key is NOT persisted (ephemeral dev keys only).
 */

import { Pool, QueryResult } from 'pg';
import { generateKeyPairSync } from 'crypto';
import nodeVault from 'node-vault';
import { RedisClientType } from 'redis';
import { ulid } from 'ulid';

import { VaultClient } from '../vault/VaultClient.js';
import { EncryptionService } from './EncryptionService.js';
import { AgentNotFoundError } from '../utils/errors.js';
import {
  IDIDDocument,
  IDIDResolutionResult,
  IAgentCard,
  IPublicKeyJwk,
  IVerificationMethod,
  IDIDService,
  IAgntcyExtension,
} from '../types/did.js';
import { IAgent } from '../types/index.js';

/** Raw row from the agents ⨯ agent_did_keys join query. */
interface AgentWithDIDKeyRow {
  agent_id: string;
  organization_id: string;
  email: string;
  agent_type: string;
  version: string;
  capabilities: string[];
  owner: string;
  deployment_env: string;
  status: string;
  created_at: Date;
  updated_at: Date;
  did: string | null;
  did_created_at: Date | null;
  key_id: string | null;
  public_key_jwk: IPublicKeyJwk | null;
  vault_key_path: string | null;
  key_type: string | null;
  curve: string | null;
  key_created_at: Date | null;
  is_public: boolean | null;
}

/** Result of the internal agent+key lookup. */
interface AgentWithDIDKey {
  agent: IAgent;
  keyId: string | null;
  publicKeyJwk: IPublicKeyJwk | null;
  vaultKeyPath: string | null;
  keyType: string | null;
  curve: string | null;
  keyCreatedAt: Date | null;
}

/** DID Document build result including deactivation metadata. */
export interface IAgentDIDDocumentResult {
  document: IDIDDocument;
  deactivated: boolean;
  createdAt: Date;
  updatedAt: Date;
}

const DID_CONTEXT_BASE = 'https://www.w3.org/ns/did/v1';
const DID_CONTEXT_AGNTCY = 'https://w3id.org/agntcy/v1';
const IDENTITY_PROVIDER_URL = 'https://idp.sentryagent.ai';

/**
 * Service for W3C DID Core 1.0 document management, key generation, and AGNTCY agent cards.
 * Integrates with Vault for private key storage and Redis for DID document caching.
 */
export class DIDService {
  /**
   * @param pool - PostgreSQL connection pool.
   * @param _vaultClient - Optional VaultClient; retained for API consistency and future use.
   *                       DID private keys are stored via node-vault directly using env vars.
   * @param redis - Redis client for DID document caching.
   * @param encryptionService - Optional EncryptionService. When provided, vault_key_path
   *                            is encrypted before write and decrypted before use (SOC 2 CC6.1).
   */
  constructor(
    private readonly pool: Pool,
    // VaultClient is accepted for API consistency and future use (e.g. token-based Vault auth).
    // DID private keys are stored via node-vault directly using env vars — see storePrivateKey().
    _vaultClient: VaultClient | null,
    private readonly redis: RedisClientType,
    private readonly encryptionService: EncryptionService | null = null,
  ) {}

  // ─────────────────────────────────────────────────────────────────────────────
  // Public API
  // ─────────────────────────────────────────────────────────────────────────────

  /**
   * Generates an EC P-256 key pair for a new agent.
   * Stores the private key in Vault (or marks as dev:no-vault in dev mode).
   * Stores the public key JWK in agent_did_keys table.
   * Populates agent.did and agent.did_created_at in the agents table.
   *
   * @param agentId - The agent UUID.
   * @param organizationId - The organization UUID.
   * @returns The generated DID and the public key JWK.
   */
  async generateDIDForAgent(
    agentId: string,
    organizationId: string,
  ): Promise<{ did: string; publicKeyJwk: IPublicKeyJwk }> {
    const { publicKey, privateKey } = generateKeyPairSync('ec', {
      namedCurve: 'P-256',
    });

    const publicKeyJwk = publicKey.export({ format: 'jwk' }) as IPublicKeyJwk;
    const privateKeyPem = privateKey.export({ format: 'pem', type: 'pkcs8' }) as string;

    const did = this.buildAgentDID(agentId);

    // Store private key — Vault if configured, dev marker otherwise
    const vaultKeyPath = await this.storePrivateKey(agentId, privateKeyPem);

    // Encrypt vault_key_path before persisting (SOC 2 CC6.1)
    const storedKeyPath =
      this.encryptionService !== null && vaultKeyPath !== 'dev:no-vault'
        ? await this.encryptionService.encryptColumn(vaultKeyPath)
        : vaultKeyPath;

    const keyId = 'key_' + ulid();

    // Insert into agent_did_keys
    await this.pool.query(
      `INSERT INTO agent_did_keys
         (key_id, agent_id, organization_id, public_key_jwk, vault_key_path, key_type, curve, created_at)
       VALUES ($1, $2, $3, $4, $5, 'EC', 'P-256', NOW())`,
      [keyId, agentId, organizationId, JSON.stringify(publicKeyJwk), storedKeyPath],
    );

    // Update agents with the DID
    await this.pool.query(
      `UPDATE agents SET did = $1, did_created_at = NOW() WHERE agent_id = $2`,
      [did, agentId],
    );

    return { did, publicKeyJwk };
  }

  /**
   * Builds and returns the root instance DID Document for AgentIdP itself.
   * Uses the DID_WEB_DOMAIN environment variable to construct the DID.
   * Result is cached in Redis under `did:doc:instance`.
   *
   * @returns The instance-level W3C DID Document.
   * @throws Error if DID_WEB_DOMAIN is not configured.
   */
  async buildInstanceDIDDocument(): Promise<IDIDDocument> {
    const cacheKey = 'did:doc:instance';
    const cached = await this.getCachedDoc(cacheKey);
    if (cached !== null) {
      return cached;
    }

    const instanceDid = this.buildInstanceDID();
    const domain = this.getDIDWebDomain();

    const vmId = `${instanceDid}#keys-1`;
    const verificationMethod: IVerificationMethod = {
      id: vmId,
      type: 'JsonWebKey2020',
      controller: instanceDid,
      publicKeyJwk: {
        kty: 'EC',
        crv: 'P-256',
        use: 'sig',
      },
    };

    const serviceEndpoint: IDIDService = {
      id: `${instanceDid}#agent-registry`,
      type: 'AgentIdentityProvider',
      serviceEndpoint: `https://${domain}/api/v1`,
    };

    const doc: IDIDDocument = {
      '@context': [DID_CONTEXT_BASE],
      id: instanceDid,
      controller: instanceDid,
      verificationMethod: [verificationMethod],
      authentication: [vmId],
      assertionMethod: [vmId],
      service: [serviceEndpoint],
    };

    await this.cacheDoc(cacheKey, doc);
    return doc;
  }

  /**
   * Builds and returns a per-agent DID Document.
   * Returns a deactivated document structure if the agent is decommissioned.
   * Result is cached in Redis under `did:doc:{agentId}`.
   *
   * @param agentId - The agent UUID.
   * @returns DID Document, deactivation flag, and timestamps.
   * @throws AgentNotFoundError if the agent does not exist.
   */
  async buildAgentDIDDocument(agentId: string): Promise<IAgentDIDDocumentResult> {
    const cacheKey = `did:doc:${agentId}`;

    const agentWithKey = await this.getAgentWithDIDKey(agentId);
    if (!agentWithKey) {
      throw new AgentNotFoundError(agentId);
    }

    const { agent, publicKeyJwk, keyCreatedAt } = agentWithKey;
    const deactivated = agent.status === 'decommissioned';

    // For decommissioned agents, do not serve stale cached documents
    if (!deactivated) {
      const cached = await this.getCachedDoc(cacheKey);
      if (cached !== null) {
        return {
          document: cached,
          deactivated: false,
          createdAt: keyCreatedAt ?? agent.createdAt,
          updatedAt: agent.updatedAt,
        };
      }
    }

    const agentDid = agent.did ?? this.buildAgentDID(agentId);
    const instanceDid = this.buildInstanceDID();
    const vmId = `${agentDid}#keys-1`;

    const verificationMethods: IVerificationMethod[] = [];

    if (publicKeyJwk !== null) {
      verificationMethods.push({
        id: vmId,
        type: 'JsonWebKey2020',
        controller: agentDid,
        publicKeyJwk,
      });
    }

    const services: IDIDService[] = [];

    if (deactivated) {
      services.push({
        id: `${agentDid}#status`,
        type: 'AgentStatus',
        serviceEndpoint: 'decommissioned',
      });
    }

    const agntcyExtension: IAgntcyExtension = {
      agentId: agent.agentId,
      agentType: agent.agentType,
      capabilities: agent.capabilities,
      deploymentEnv: agent.deploymentEnv,
      owner: agent.owner,
      version: agent.version,
    };

    const doc: IDIDDocument = {
      '@context': [DID_CONTEXT_BASE, DID_CONTEXT_AGNTCY],
      id: agentDid,
      controller: instanceDid,
      verificationMethod: verificationMethods,
      authentication: publicKeyJwk !== null ? [vmId] : [],
      assertionMethod: publicKeyJwk !== null ? [vmId] : [],
      service: services.length > 0 ? services : undefined,
      agntcy: agntcyExtension,
    };

    if (!deactivated) {
      await this.cacheDoc(cacheKey, doc);
    }

    return {
      document: doc,
      deactivated,
      createdAt: keyCreatedAt ?? agent.createdAt,
      updatedAt: agent.updatedAt,
    };
  }

  /**
   * Builds the W3C DID Resolution result for a given agent.
   * Wraps the DID Document with resolution and document metadata.
   *
   * @param agentId - The agent UUID.
   * @returns A complete W3C DID Resolution result.
   * @throws AgentNotFoundError if the agent does not exist.
   */
  async buildResolutionResult(agentId: string): Promise<IDIDResolutionResult> {
    const { document, deactivated, createdAt, updatedAt } =
      await this.buildAgentDIDDocument(agentId);

    return {
      didDocument: document,
      didDocumentMetadata: {
        created: createdAt.toISOString(),
        updated: updatedAt.toISOString(),
        deactivated,
      },
      didResolutionMetadata: {
        contentType: 'application/did+ld+json',
        retrieved: new Date().toISOString(),
      },
    };
  }

  /**
   * Builds an AGNTCY-format agent card from the agent's DID Document.
   * The agent card provides a machine-readable identity summary for AGNTCY consumers.
   *
   * @param agentId - The agent UUID.
   * @returns An AGNTCY agent card.
   * @throws AgentNotFoundError if the agent does not exist.
   */
  async buildAgentCard(agentId: string): Promise<IAgentCard> {
    const agentWithKey = await this.getAgentWithDIDKey(agentId);
    if (!agentWithKey) {
      throw new AgentNotFoundError(agentId);
    }

    const { agent, keyCreatedAt } = agentWithKey;
    const agentDid = agent.did ?? this.buildAgentDID(agentId);

    return {
      did: agentDid,
      name: agent.email,
      agentType: agent.agentType,
      capabilities: agent.capabilities,
      owner: agent.owner,
      version: agent.version,
      deploymentEnv: agent.deploymentEnv,
      identityProvider: IDENTITY_PROVIDER_URL,
      issuedAt: (keyCreatedAt ?? agent.createdAt).toISOString(),
    };
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Private helpers
  // ─────────────────────────────────────────────────────────────────────────────

  /**
   * Reads DID_WEB_DOMAIN from the environment.
   *
   * @returns The configured DID Web domain (e.g. `idp.sentryagent.ai`).
   * @throws Error if DID_WEB_DOMAIN is not set.
   */
  private getDIDWebDomain(): string {
    const domain = process.env['DID_WEB_DOMAIN'];
    if (!domain) {
      throw new Error('DID_WEB_DOMAIN environment variable is required');
    }
    return domain;
  }

  /**
   * Builds the per-agent DID using the did:web method.
   * Format: `did:web:{domain}:agents:{agentId}`
   *
   * @param agentId - The agent UUID.
   * @returns The did:web DID string for the agent.
   */
  private buildAgentDID(agentId: string): string {
    const domain = this.getDIDWebDomain();
    return `did:web:${domain}:agents:${agentId}`;
  }

  /**
   * Builds the instance-level DID for AgentIdP itself.
   * Format: `did:web:{domain}`
   *
   * @returns The did:web DID string for the AgentIdP instance.
   */
  private buildInstanceDID(): string {
    const domain = this.getDIDWebDomain();
    return `did:web:${domain}`;
  }

  /**
   * Retrieves a cached DID Document from Redis.
   *
   * @param key - Redis cache key.
   * @returns The cached DID Document, or null if not found.
   */
  private async getCachedDoc(key: string): Promise<IDIDDocument | null> {
    const raw = await this.redis.get(key);
    if (raw === null) return null;
    try {
      return JSON.parse(raw) as IDIDDocument;
    } catch {
      return null;
    }
  }

  /**
   * Stores a DID Document in Redis with the configured TTL.
   * TTL is read from DID_DOCUMENT_CACHE_TTL_SECONDS env (default: 300).
   *
   * @param key - Redis cache key.
   * @param doc - The DID Document to cache.
   */
  private async cacheDoc(key: string, doc: IDIDDocument): Promise<void> {
    const ttl = parseInt(process.env['DID_DOCUMENT_CACHE_TTL_SECONDS'] ?? '300', 10);
    await this.redis.set(key, JSON.stringify(doc), { EX: ttl });
  }

  /**
   * Fetches an agent record joined with its DID key from the database.
   *
   * @param agentId - The agent UUID.
   * @returns The agent with its associated DID key row, or null if not found.
   */
  private async getAgentWithDIDKey(agentId: string): Promise<AgentWithDIDKey | null> {
    const result: QueryResult<AgentWithDIDKeyRow> = await this.pool.query(
      `SELECT
         a.agent_id, a.organization_id, a.email, a.agent_type, a.version,
         a.capabilities, a.owner, a.deployment_env, a.status,
         a.created_at, a.updated_at, a.did, a.did_created_at,
         k.key_id, k.public_key_jwk, k.vault_key_path,
         k.key_type, k.curve, k.created_at AS key_created_at
       FROM agents a
       LEFT JOIN agent_did_keys k ON k.agent_id = a.agent_id
       WHERE a.agent_id = $1`,
      [agentId],
    );

    if (result.rows.length === 0) return null;

    const row = result.rows[0];

    const agent: IAgent = {
      agentId: row.agent_id,
      organizationId: row.organization_id,
      email: row.email,
      agentType: row.agent_type as IAgent['agentType'],
      version: row.version,
      capabilities: row.capabilities,
      owner: row.owner,
      deploymentEnv: row.deployment_env as IAgent['deploymentEnv'],
      status: row.status as IAgent['status'],
      createdAt: row.created_at,
      updatedAt: row.updated_at,
      did: row.did ?? undefined,
      didCreatedAt: row.did_created_at ?? undefined,
      isPublic: row.is_public ?? false,
    };

    return {
      agent,
      keyId: row.key_id,
      publicKeyJwk: row.public_key_jwk,
      vaultKeyPath: row.vault_key_path,
      keyType: row.key_type,
      curve: row.curve,
      keyCreatedAt: row.key_created_at,
    };
  }

  /**
   * Stores a DID private key PEM in Vault (if configured) or records a dev marker.
   *
   * When VAULT_ADDR and VAULT_TOKEN are set, the private key is written to Vault KV v2
   * at `{mount}/data/agentidp/agents/{agentId}/did-key` and the Vault path is returned.
   *
   * When Vault is not configured (dev/test mode), the private key is NOT persisted and
   * the string `dev:no-vault` is returned as the path marker. This is safe for local
   * development only — keys cannot be recovered in this mode.
   *
   * @param agentId - The agent UUID (used to construct the Vault path).
   * @param privateKeyPem - The PKCS#8 PEM-encoded private key to store.
   * @returns The Vault path where the key was stored, or `dev:no-vault`.
   */
  private async storePrivateKey(agentId: string, privateKeyPem: string): Promise<string> {
    const vaultAddr = process.env['VAULT_ADDR'];
    const vaultToken = process.env['VAULT_TOKEN'];

    if (vaultAddr && vaultToken) {
      const mount = process.env['VAULT_MOUNT'] ?? 'secret';
      const vaultPath = `${mount}/data/agentidp/agents/${agentId}/did-key`;
      const vault = nodeVault({ endpoint: vaultAddr, token: vaultToken });
      await vault.write(vaultPath, { data: { privateKeyPem } });
      return vaultPath;
    }

    // Dev mode: private key is not persisted — only a marker is stored
    return 'dev:no-vault';
  }
}
