/**
 * Analytics Service for SentryAgent.ai AgentIdP.
 * Records daily aggregated analytics events and exposes query methods
 * for token trends, agent activity heatmaps, and per-agent usage summaries.
 *
 * All query methods scope results strictly to the supplied tenantId.
 * The recordEvent method is fire-and-forget — it catches all errors internally
 * and never propagates them to the caller.
 */

import { Pool } from 'pg';

/** A single date-bucketed token count entry. */
export interface ITokenTrendEntry {
  date: string;
  count: number;
}

/** Agent activity bucketed by day-of-week and hour-of-day. */
export interface IAgentActivityEntry {
  agent_id: string;
  dow: number;
  hour: number;
  count: number;
}

/** Per-agent token issuance summary for the current calendar month. */
export interface IAgentUsageSummaryEntry {
  agent_id: string;
  name: string;
  token_count: number;
}

/** Maximum number of days allowed for trend queries. */
const MAX_TREND_DAYS = 90;

/**
 * Service for recording and querying tenant analytics events.
 * Analytics writes are fire-and-forget and never block primary request paths.
 */
export class AnalyticsService {
  /**
   * @param pool - The PostgreSQL connection pool.
   */
  constructor(private readonly pool: Pool) {}

  /**
   * Records a single analytics event for a tenant by upserting a daily counter row.
   * This method is fire-and-forget: it catches all errors, logs them, and never throws.
   *
   * @param tenantId - The organization_id of the tenant.
   * @param metricType - The event type (e.g. 'token_issued', 'agent_registered').
   * @returns Promise that resolves when the upsert completes (or is silently swallowed on error).
   */
  async recordEvent(tenantId: string, metricType: string): Promise<void> {
    try {
      await this.pool.query(
        `INSERT INTO analytics_events (organization_id, date, metric_type, count)
         VALUES ($1, CURRENT_DATE, $2, 1)
         ON CONFLICT (organization_id, date, metric_type)
         DO UPDATE SET count = analytics_events.count + 1`,
        [tenantId, metricType],
      );
    } catch (err) {
      // eslint-disable-next-line no-console
      console.error('[AnalyticsService] recordEvent failed — primary path unaffected', err);
    }
  }

  /**
   * Returns daily token issuance counts for the last N days (max 90).
   * Days with no recorded events are filled in with a count of 0.
   *
   * @param tenantId - The organization_id of the tenant.
   * @param days - Number of days to look back (1–90).
   * @returns Array of date/count entries sorted ascending by date.
   */
  async getTokenTrend(tenantId: string, days: number): Promise<ITokenTrendEntry[]> {
    const clampedDays = Math.min(days, MAX_TREND_DAYS);

    // Generate a complete date series and left-join analytics data so that
    // days with no events appear as 0.
    const result = await this.pool.query<{ date: string; count: string }>(
      `SELECT
         gs.date::DATE::TEXT AS date,
         COALESCE(ae.count, 0)::INTEGER AS count
       FROM generate_series(
         CURRENT_DATE - ($1::INTEGER - 1) * INTERVAL '1 day',
         CURRENT_DATE,
         INTERVAL '1 day'
       ) AS gs(date)
       LEFT JOIN analytics_events ae
         ON ae.date = gs.date::DATE
         AND ae.organization_id = $2
         AND ae.metric_type = 'token_issued'
       ORDER BY gs.date ASC`,
      [clampedDays, tenantId],
    );

    return result.rows.map((row) => ({
      date: row.date,
      count: Number(row.count),
    }));
  }

  /**
   * Returns agent activity bucketed by day-of-week (0=Sun…6=Sat) and hour-of-day
   * for the last 30 days. Only metric_types that include an agent_id prefix
   * (format: 'agent:<agentId>:<metricType>') are included.
   *
   * Since analytics_events stores daily aggregates, DOW/hour granularity is derived
   * from the event date at day resolution (hour defaults to 0 for daily rollups).
   *
   * @param tenantId - The organization_id of the tenant.
   * @returns Array of activity bucket entries sorted by agent_id, dow, hour.
   */
  async getAgentActivity(tenantId: string): Promise<IAgentActivityEntry[]> {
    const result = await this.pool.query<{
      agent_id: string;
      dow: string;
      hour: string;
      count: string;
    }>(
      `SELECT
         SPLIT_PART(ae.metric_type, ':', 2)            AS agent_id,
         EXTRACT(DOW  FROM ae.date)::INTEGER::TEXT      AS dow,
         0::TEXT                                         AS hour,
         SUM(ae.count)::TEXT                             AS count
       FROM analytics_events ae
       WHERE ae.organization_id = $1
         AND ae.date >= CURRENT_DATE - INTERVAL '30 days'
         AND ae.metric_type LIKE 'agent:%:%'
       GROUP BY
         SPLIT_PART(ae.metric_type, ':', 2),
         EXTRACT(DOW FROM ae.date)
       ORDER BY agent_id, dow`,
      [tenantId],
    );

    return result.rows.map((row) => ({
      agent_id: row.agent_id,
      dow: Number(row.dow),
      hour: Number(row.hour),
      count: Number(row.count),
    }));
  }

  /**
   * Returns per-agent token issuance totals for the current calendar month,
   * joined with the agent name from the agents table.
   * Results are sorted descending by token_count.
   *
   * @param tenantId - The organization_id of the tenant.
   * @returns Array of agent usage summary entries.
   */
  async getAgentUsageSummary(tenantId: string): Promise<IAgentUsageSummaryEntry[]> {
    const result = await this.pool.query<{
      agent_id: string;
      name: string;
      token_count: string;
    }>(
      `SELECT
         a.agent_id,
         a.owner AS name,
         COALESCE(SUM(ae.count), 0)::INTEGER AS token_count
       FROM agents a
       LEFT JOIN analytics_events ae
         ON ae.organization_id = a.organization_id
         AND ae.metric_type = 'token_issued'
         AND ae.date >= DATE_TRUNC('month', CURRENT_DATE)
         AND ae.date <  DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month'
       WHERE a.organization_id = $1
         AND a.status != 'decommissioned'
       GROUP BY a.agent_id, a.owner
       ORDER BY token_count DESC`,
      [tenantId],
    );

    return result.rows.map((row) => ({
      agent_id: row.agent_id,
      name: row.name,
      token_count: Number(row.token_count),
    }));
  }
}
