/**
 * Agent Registry Service for SentryAgent.ai AgentIdP.
 * Business logic for agent lifecycle management.
 */

import { AgentRepository } from '../repositories/AgentRepository.js';
import { CredentialRepository } from '../repositories/CredentialRepository.js';
import { AuditService } from './AuditService.js';
import { DIDService } from './DIDService.js';
import { EventPublisher } from './EventPublisher.js';
import { AnalyticsService } from './AnalyticsService.js';
import {
  IAgent,
  ICreateAgentRequest,
  IUpdateAgentRequest,
  IAgentListFilters,
  IPaginatedAgentsResponse,
} from '../types/index.js';
import {
  AgentNotFoundError,
  AgentAlreadyExistsError,
  AgentAlreadyDecommissionedError,
  FreeTierLimitError,
} from '../utils/errors.js';
import { agentsRegisteredTotal } from '../metrics/registry.js';
import { TierService } from './TierService.js';

const FREE_TIER_MAX_AGENTS = 100;

/**
 * Service for agent registration and lifecycle management.
 * Enforces free-tier limits and coordinates with AuditService.
 */
export class AgentService {
  /**
   * @param agentRepository - The agent data repository.
   * @param credentialRepository - The credential repository (for decommission cleanup).
   * @param auditService - The audit log service.
   * @param didService - Optional DIDService. When provided, a W3C DID is generated for each
   *                     newly registered agent. When null/undefined, DID generation is skipped
   *                     (backward-compatible default).
   * @param eventPublisher - Optional EventPublisher. When provided, lifecycle events are
   *                         published as webhooks and Kafka messages (fire-and-forget).
   * @param analyticsService - Optional AnalyticsService. When provided, agent_registered
   *                           and agent_deactivated events are recorded fire-and-forget.
   * @param tierService - Optional TierService. When provided, per-tier agent count limits
   *                      are enforced at agent creation time (Phase 6 WS4).
   */
  constructor(
    private readonly agentRepository: AgentRepository,
    private readonly credentialRepository: CredentialRepository,
    private readonly auditService: AuditService,
    private readonly didService: DIDService | null = null,
    private readonly eventPublisher: EventPublisher | null = null,
    private readonly analyticsService: AnalyticsService | null = null,
    private readonly tierService: TierService | null = null,
  ) {}

  /**
   * Registers a new AI agent identity.
   * Enforces the free-tier 100-agent limit and unique email constraint.
   *
   * @param data - The agent registration data.
   * @param ipAddress - Client IP for audit logging.
   * @param userAgent - Client User-Agent for audit logging.
   * @returns The newly created agent record.
   * @throws FreeTierLimitError if the 100-agent limit is reached.
   * @throws AgentAlreadyExistsError if the email is already registered.
   */
  async registerAgent(
    data: ICreateAgentRequest,
    ipAddress: string,
    userAgent: string,
  ): Promise<IAgent> {
    const orgId = data.organizationId ?? 'org_system';

    // ── Tier-based agent count enforcement (Phase 6 WS4) ────────────────────
    // When TierService is available and TIER_ENFORCEMENT is enabled, validate
    // the per-tier agent limit for the requesting organization.
    if (this.tierService !== null && process.env['TIER_ENFORCEMENT'] !== 'false') {
      const tier = await this.tierService.fetchTier(orgId);
      await this.tierService.enforceAgentLimit(orgId, tier);
    }

    // Enforce legacy free-tier agent count limit (global across all orgs)
    const currentCount = await this.agentRepository.countActive();
    if (currentCount >= FREE_TIER_MAX_AGENTS) {
      throw new FreeTierLimitError(
        'Free tier limit of 100 registered agents has been reached.',
        { limit: FREE_TIER_MAX_AGENTS, current: currentCount },
      );
    }

    // Check email uniqueness
    const existing = await this.agentRepository.findByEmail(data.email);
    if (existing !== null) {
      throw new AgentAlreadyExistsError(data.email);
    }

    const agent = await this.agentRepository.create(data);

    // Generate a W3C DID for the new agent when DIDService is available
    if (this.didService !== null) {
      await this.didService.generateDIDForAgent(agent.agentId, orgId);
    }

    // Synchronous audit insert
    await this.auditService.logEvent(
      agent.agentId,
      'agent.created',
      'success',
      ipAddress,
      userAgent,
      { agentType: agent.agentType, owner: agent.owner },
    );

    // Instrument: count successful agent registrations
    agentsRegisteredTotal.inc({ deployment_env: data.deploymentEnv });

    // Analytics: record agent_registered event (fire-and-forget)
    if (this.analyticsService !== null) {
      void this.analyticsService.recordEvent(
        agent.organizationId ?? 'org_system',
        'agent_registered',
      ).catch((err: unknown) => {
        // eslint-disable-next-line no-console
        console.error('[AgentService] analytics record (agent_registered) failed', err);
      });
    }

    // Publish event (fire-and-forget)
    void this.eventPublisher?.publishEvent(
      agent.organizationId,
      'agent.created',
      { agentId: agent.agentId, email: agent.email, name: agent.owner },
    );

    return agent;
  }

  /**
   * Retrieves a single agent by its UUID.
   *
   * @param agentId - The agent UUID.
   * @returns The agent record.
   * @throws AgentNotFoundError if the agent does not exist.
   */
  async getAgentById(agentId: string): Promise<IAgent> {
    const agent = await this.agentRepository.findById(agentId);
    if (!agent) {
      throw new AgentNotFoundError(agentId);
    }
    return agent;
  }

  /**
   * Returns a paginated, optionally filtered list of agents.
   *
   * @param filters - Pagination and filter criteria.
   * @returns Paginated agents response.
   */
  async listAgents(filters: IAgentListFilters): Promise<IPaginatedAgentsResponse> {
    const { agents, total } = await this.agentRepository.findAll(filters);
    return {
      data: agents,
      total,
      page: filters.page,
      limit: filters.limit,
    };
  }

  /**
   * Partially updates an agent's metadata.
   * Immutable fields (agentId, email, createdAt) cannot be changed.
   * Decommissioned agents cannot be updated.
   *
   * @param agentId - The agent UUID to update.
   * @param data - The fields to update.
   * @param ipAddress - Client IP for audit logging.
   * @param userAgent - Client User-Agent for audit logging.
   * @returns The updated agent record.
   * @throws AgentNotFoundError if the agent does not exist.
   * @throws AgentAlreadyDecommissionedError if the agent is decommissioned.
   * @throws ValidationError if immutable fields are included.
   */
  async updateAgent(
    agentId: string,
    data: IUpdateAgentRequest,
    ipAddress: string,
    userAgent: string,
  ): Promise<IAgent> {
    const agent = await this.agentRepository.findById(agentId);
    if (!agent) {
      throw new AgentNotFoundError(agentId);
    }

    if (agent.status === 'decommissioned') {
      throw new AgentAlreadyDecommissionedError(agentId);
    }

    // Detect if status changes
    const oldStatus = agent.status;
    const updated = await this.agentRepository.update(agentId, data);
    if (!updated) {
      throw new AgentNotFoundError(agentId);
    }

    // Determine which audit action to log
    let auditAction: 'agent.updated' | 'agent.suspended' | 'agent.reactivated' | 'agent.decommissioned' =
      'agent.updated';
    if (data.status !== undefined && data.status !== oldStatus) {
      if (data.status === 'suspended') auditAction = 'agent.suspended';
      else if (data.status === 'active') auditAction = 'agent.reactivated';
      else if (data.status === 'decommissioned') auditAction = 'agent.decommissioned';
    }

    await this.auditService.logEvent(
      agentId,
      auditAction,
      'success',
      ipAddress,
      userAgent,
      { updatedFields: Object.keys(data) },
    );

    // Publish lifecycle event (fire-and-forget)
    if (auditAction === 'agent.updated') {
      void this.eventPublisher?.publishEvent(
        updated.organizationId,
        'agent.updated',
        { agentId, changes: data },
      );
    } else if (auditAction === 'agent.suspended') {
      void this.eventPublisher?.publishEvent(
        updated.organizationId,
        'agent.suspended',
        { agentId },
      );
    } else if (auditAction === 'agent.reactivated') {
      void this.eventPublisher?.publishEvent(
        updated.organizationId,
        'agent.reactivated',
        { agentId },
      );
    } else if (auditAction === 'agent.decommissioned') {
      void this.eventPublisher?.publishEvent(
        updated.organizationId,
        'agent.decommissioned',
        { agentId },
      );
    }

    return updated;
  }

  /**
   * Permanently decommissions an agent (soft delete).
   * Revokes all active credentials for the agent.
   *
   * @param agentId - The agent UUID to decommission.
   * @param ipAddress - Client IP for audit logging.
   * @param userAgent - Client User-Agent for audit logging.
   * @throws AgentNotFoundError if the agent does not exist.
   * @throws AgentAlreadyDecommissionedError if already decommissioned.
   */
  async decommissionAgent(
    agentId: string,
    ipAddress: string,
    userAgent: string,
  ): Promise<void> {
    const agent = await this.agentRepository.findById(agentId);
    if (!agent) {
      throw new AgentNotFoundError(agentId);
    }

    if (agent.status === 'decommissioned') {
      throw new AgentAlreadyDecommissionedError(agentId);
    }

    // Revoke all active credentials
    await this.credentialRepository.revokeAllForAgent(agentId);

    await this.agentRepository.decommission(agentId);

    await this.auditService.logEvent(
      agentId,
      'agent.decommissioned',
      'success',
      ipAddress,
      userAgent,
      {},
    );

    // Analytics: record agent_deactivated event (fire-and-forget)
    if (this.analyticsService !== null) {
      void this.analyticsService.recordEvent(
        agent.organizationId ?? 'org_system',
        'agent_deactivated',
      ).catch((err: unknown) => {
        // eslint-disable-next-line no-console
        console.error('[AgentService] analytics record (agent_deactivated) failed', err);
      });
    }

    // Publish event (fire-and-forget)
    void this.eventPublisher?.publishEvent(
      agent.organizationId,
      'agent.decommissioned',
      { agentId },
    );
  }
}
