import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { AudienceQueryDto, GeoQueryDto, DeviceQueryDto, GeoGranularity } from './dto/audience-query.dto';
import { resolveCorpId, corpSessionFilter } from '../common/corp-filter.util';

export interface AudienceOverview {
  totalUsers: number;
  newUsers: number;
  returningUsers: number;
  newUserRate: number;
  avgSessionsPerUser: number;
  topCountries: GeoMetrics[];
  topDevices: DeviceMetrics[];
}

export interface DemographicsMetrics {
  userType: 'new' | 'returning';
  users: number;
  sessions: number;
  engagementRate: number;
  conversionRate: number;
  revenuePerUser: number;
}

export interface DeviceMetrics {
  deviceType: string;
  sessions: number;
  users: number;
  percentage: number;
  engagementRate: number;
  conversionRate: number;
}

export interface BrowserMetrics {
  browser: string;
  version: string;
  sessions: number;
  users: number;
  percentage: number;
}

export interface OSMetrics {
  os: string;
  version: string;
  sessions: number;
  users: number;
  percentage: number;
}

export interface GeoMetrics {
  location: string;
  sessions: number;
  users: number;
  percentage: number;
  engagementRate: number;
  conversionRate: number;
}

export interface LanguageMetrics {
  language: string;
  sessions: number;
  users: number;
  percentage: number;
}

export interface NewVsReturningMetrics {
  period: string;
  newUsers: number;
  returningUsers: number;
  newUserRate: number;
}

/**
 * Audience service for demographics analytics.
 * Queries session_fingerprints for device/geo data.
 */
@Injectable()
export class AudienceService {
  private readonly logger = new Logger(AudienceService.name);

  constructor(
    @InjectDataSource()
    private readonly dataSource: DataSource,
  ) {}

  /**
   * Get audience overview with top countries and devices
   */
  async getOverview(query: AudienceQueryDto): Promise<AudienceOverview> {
    const { startDate, endDate, limit } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(3, corpId);

    const overviewQuery = `
      WITH user_sessions AS (
        SELECT
          sf."userId",
          sf."sessionId",
          sf."deviceType",
          sf.country,
          sf."createdAt"
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false${corpClause}
      ),
      user_first AS (
        SELECT "userId", MIN("createdAt") as first_session
        FROM session_fingerprints
        WHERE "userId" IS NOT NULL
        GROUP BY "userId"
      )
      SELECT
        COUNT(DISTINCT us."userId") as total_users,
        COUNT(DISTINCT CASE WHEN uf.first_session >= $1 THEN us."userId" END) as new_users,
        COUNT(DISTINCT CASE WHEN uf.first_session < $1 THEN us."userId" END) as returning_users,
        COUNT(us."sessionId")::float / NULLIF(COUNT(DISTINCT us."userId"), 0) as avg_sessions_per_user
      FROM user_sessions us
      LEFT JOIN user_first uf ON us."userId" = uf."userId"
    `;

    const overviewParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];

    const [overview, topCountries, topDevices] = await Promise.all([
      this.dataSource.query(overviewQuery, overviewParams),
      this.getGeography({ ...query, granularity: GeoGranularity.COUNTRY, limit: 5 }),
      this.getDevices({ ...query, limit: 5 }),
    ]);

    const row = overview[0] || {};
    const totalUsers = Number(row.total_users) || 0;
    const newUsers = Number(row.new_users) || 0;
    const returningUsers = Number(row.returning_users) || 0;

    return {
      totalUsers,
      newUsers,
      returningUsers,
      newUserRate: totalUsers > 0 ? newUsers / totalUsers : 0,
      avgSessionsPerUser: Number(row.avg_sessions_per_user) || 0,
      topCountries,
      topDevices,
    };
  }

  /**
   * Get demographics breakdown (new vs returning)
   */
  async getDemographics(query: AudienceQueryDto): Promise<DemographicsMetrics[]> {
    const { startDate, endDate } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(3, corpId);

    const demographicsQuery = `
      WITH user_sessions AS (
        SELECT
          sf."userId",
          sf."sessionId",
          sf."createdAt",
          EXISTS (
            SELECT 1 FROM raw_events re
            WHERE re."sessionId" = sf."sessionId"
            AND re."eventType" IN ('conversion', 'purchase')
          ) as has_conversion,
          COALESCE((
            SELECT EXTRACT(EPOCH FROM (MAX(re.timestamp) - MIN(re.timestamp)))
            FROM raw_events re WHERE re."sessionId" = sf."sessionId"
          ), 0) as duration_seconds,
          COALESCE((
            SELECT COUNT(*) FROM raw_events re
            WHERE re."sessionId" = sf."sessionId" AND re."eventType" IN ('pageView', 'pageview')
          ), 0) as page_views,
          COALESCE((
            SELECT SUM((re.metadata->>'revenue')::numeric)
            FROM raw_events re
            WHERE re."sessionId" = sf."sessionId" AND re."eventType" IN ('conversion', 'purchase')
          ), 0) as revenue
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false
          AND sf."userId" IS NOT NULL${corpClause}
      ),
      user_first AS (
        SELECT "userId", MIN("createdAt") as first_session
        FROM session_fingerprints
        WHERE "userId" IS NOT NULL
        GROUP BY "userId"
      ),
      classified AS (
        SELECT
          us.*,
          CASE WHEN uf.first_session >= $1 THEN 'new' ELSE 'returning' END as user_type
        FROM user_sessions us
        LEFT JOIN user_first uf ON us."userId" = uf."userId"
      )
      SELECT
        user_type,
        COUNT(DISTINCT "userId") as users,
        COUNT("sessionId") as sessions,
        COUNT(CASE WHEN duration_seconds >= 10 OR page_views >= 2 OR has_conversion THEN 1 END)::float / NULLIF(COUNT(*), 0) as engagement_rate,
        COUNT(CASE WHEN has_conversion THEN 1 END)::float / NULLIF(COUNT(*), 0) as conversion_rate,
        SUM(revenue) / NULLIF(COUNT(DISTINCT "userId"), 0) as revenue_per_user
      FROM classified
      GROUP BY user_type
    `;

    try {
      const demoParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
      const result = await this.dataSource.query(demographicsQuery, demoParams);

      return result.map((row: Record<string, unknown>) => ({
        userType: row.user_type as 'new' | 'returning',
        users: Number(row.users) || 0,
        sessions: Number(row.sessions) || 0,
        engagementRate: Number(row.engagement_rate) || 0,
        conversionRate: Number(row.conversion_rate) || 0,
        revenuePerUser: Number(row.revenue_per_user) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get demographics', error);
      throw error;
    }
  }

  /**
   * Get device breakdown from aggregated_metrics (device_type dimension).
   * Falls back to raw_events when no aggregated data is available.
   */
  async getDevices(query: DeviceQueryDto): Promise<DeviceMetrics[]> {
    const { startDate, endDate, deviceType, limit } = query;

    // Use aggregated_metrics for device breakdown — avoids dependence on
    // session_fingerprints which may not have data for all sessions.
    const devicesQuery = `
      WITH device_totals AS (
        SELECT
          "dimensionValue" AS device_type,
          SUM(value) AS page_views
        FROM aggregated_metrics
        WHERE "metricType" = 'page_views'
          AND dimension = 'device_type'
          AND "dimensionValue" != 'bot'
          AND "dimensionValue" IS NOT NULL
          ${deviceType ? `AND "dimensionValue" = $4` : ''}
          AND timestamp BETWEEN $1 AND $2
        GROUP BY "dimensionValue"
      ),
      grand_total AS (
        SELECT COALESCE(SUM(page_views), 0) AS total FROM device_totals
      )
      SELECT
        dt.device_type,
        dt.page_views::bigint AS sessions,
        0::bigint AS users,
        CASE WHEN gt.total > 0 THEN dt.page_views / gt.total ELSE 0 END AS percentage,
        0.0 AS engagement_rate,
        0.0 AS conversion_rate
      FROM device_totals dt
      CROSS JOIN grand_total gt
      ORDER BY dt.page_views DESC
      LIMIT $3
    `;

    const params: (string | number)[] = [startDate, endDate, limit ?? 20];
    if (deviceType) params.push(deviceType);

    try {
      const result = await this.dataSource.query(devicesQuery, params);

      return result.map((row: Record<string, unknown>) => ({
        deviceType: row.device_type as string,
        sessions: Number(row.sessions) || 0,
        users: Number(row.users) || 0,
        percentage: Number(row.percentage) || 0,
        engagementRate: Number(row.engagement_rate) || 0,
        conversionRate: Number(row.conversion_rate) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get devices', error);
      throw error;
    }
  }

  /**
   * Get browser breakdown
   */
  async getBrowsers(query: AudienceQueryDto): Promise<BrowserMetrics[]> {
    const { startDate, endDate, limit } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(4, corpId);

    const browsersQuery = `
      WITH session_data AS (
        SELECT
          sf."sessionId",
          sf."userId",
          COALESCE(sf.browser, 'unknown') as browser,
          COALESCE(sf."browserVersion", '') as version
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false${corpClause}
      ),
      total AS (
        SELECT COUNT(*) as total_sessions FROM session_data
      )
      SELECT
        sd.browser,
        sd.version,
        COUNT(sd."sessionId") as sessions,
        COUNT(DISTINCT sd."userId") as users,
        COUNT(sd."sessionId")::float / NULLIF(t.total_sessions, 0) as percentage
      FROM session_data sd
      CROSS JOIN total t
      GROUP BY sd.browser, sd.version, t.total_sessions
      ORDER BY sessions DESC
      LIMIT $3
    `;

    try {
      const browserParams: unknown[] = corpId === null
        ? [startDate, endDate, limit ?? 20]
        : [startDate, endDate, limit ?? 20, corpId];
      const result = await this.dataSource.query(browsersQuery, browserParams);

      return result.map((row: Record<string, unknown>) => ({
        browser: row.browser as string,
        version: row.version as string,
        sessions: Number(row.sessions) || 0,
        users: Number(row.users) || 0,
        percentage: Number(row.percentage) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get browsers', error);
      throw error;
    }
  }

  /**
   * Get OS breakdown
   */
  async getOperatingSystems(query: AudienceQueryDto): Promise<OSMetrics[]> {
    const { startDate, endDate, limit } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(4, corpId);

    const osQuery = `
      WITH session_data AS (
        SELECT
          sf."sessionId",
          sf."userId",
          COALESCE(sf.os, 'unknown') as os,
          COALESCE(sf."osVersion", '') as version
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false${corpClause}
      ),
      total AS (
        SELECT COUNT(*) as total_sessions FROM session_data
      )
      SELECT
        sd.os,
        sd.version,
        COUNT(sd."sessionId") as sessions,
        COUNT(DISTINCT sd."userId") as users,
        COUNT(sd."sessionId")::float / NULLIF(t.total_sessions, 0) as percentage
      FROM session_data sd
      CROSS JOIN total t
      GROUP BY sd.os, sd.version, t.total_sessions
      ORDER BY sessions DESC
      LIMIT $3
    `;

    try {
      const osParams: unknown[] = corpId === null
        ? [startDate, endDate, limit ?? 20]
        : [startDate, endDate, limit ?? 20, corpId];
      const result = await this.dataSource.query(osQuery, osParams);

      return result.map((row: Record<string, unknown>) => ({
        os: row.os as string,
        version: row.version as string,
        sessions: Number(row.sessions) || 0,
        users: Number(row.users) || 0,
        percentage: Number(row.percentage) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get operating systems', error);
      throw error;
    }
  }

  /**
   * Get geography breakdown
   */
  async getGeography(query: GeoQueryDto): Promise<GeoMetrics[]> {
    const { startDate, endDate, granularity, country, region, limit } = query;

    const conditions: string[] = [
      'sf."createdAt" BETWEEN $1 AND $2',
      'sf."isBot" = false',
    ];
    const params: (string | number)[] = [startDate, endDate];
    let paramIndex = 3;

    if (country) {
      conditions.push(`sf.country = $${paramIndex}`);
      params.push(country);
      paramIndex++;
    }
    if (region) {
      conditions.push(`sf.region = $${paramIndex}`);
      params.push(region);
      paramIndex++;
    }

    const corpId = await resolveCorpId(this.dataSource, query.corp);
    if (corpId !== null) {
      conditions.push(`sf."sessionId" IN (SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $${paramIndex})`);
      params.push(corpId);
      paramIndex++;
    }

    const whereClause = conditions.join(' AND ');

    const locationColumn = this.getGeoColumn(granularity);

    const geoQuery = `
      WITH session_data AS (
        SELECT
          sf."sessionId",
          sf."userId",
          COALESCE(${locationColumn}, 'unknown') as location,
          EXISTS (
            SELECT 1 FROM raw_events re
            WHERE re."sessionId" = sf."sessionId"
            AND re."eventType" IN ('conversion', 'purchase')
          ) as has_conversion,
          COALESCE((
            SELECT EXTRACT(EPOCH FROM (MAX(re.timestamp) - MIN(re.timestamp)))
            FROM raw_events re WHERE re."sessionId" = sf."sessionId"
          ), 0) as duration_seconds,
          COALESCE((
            SELECT COUNT(*) FROM raw_events re
            WHERE re."sessionId" = sf."sessionId" AND re."eventType" IN ('pageView', 'pageview')
          ), 0) as page_views
        FROM session_fingerprints sf
        WHERE ${whereClause}
      ),
      total AS (
        SELECT COUNT(*) as total_sessions FROM session_data
      )
      SELECT
        sd.location,
        COUNT(sd."sessionId") as sessions,
        COUNT(DISTINCT sd."userId") as users,
        COUNT(sd."sessionId")::float / NULLIF(t.total_sessions, 0) as percentage,
        COUNT(CASE WHEN sd.duration_seconds >= 10 OR sd.page_views >= 2 OR sd.has_conversion THEN 1 END)::float / NULLIF(COUNT(*), 0) as engagement_rate,
        COUNT(CASE WHEN sd.has_conversion THEN 1 END)::float / NULLIF(COUNT(*), 0) as conversion_rate
      FROM session_data sd
      CROSS JOIN total t
      GROUP BY sd.location, t.total_sessions
      ORDER BY sessions DESC
      LIMIT $${paramIndex}
    `;

    params.push(limit ?? 20);

    try {
      const result = await this.dataSource.query(geoQuery, params);

      return result.map((row: Record<string, unknown>) => ({
        location: row.location as string,
        sessions: Number(row.sessions) || 0,
        users: Number(row.users) || 0,
        percentage: Number(row.percentage) || 0,
        engagementRate: Number(row.engagement_rate) || 0,
        conversionRate: Number(row.conversion_rate) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get geography', error);
      throw error;
    }
  }

  /**
   * Get language breakdown
   */
  async getLanguages(query: AudienceQueryDto): Promise<LanguageMetrics[]> {
    const { startDate, endDate, limit } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(4, corpId);

    const languagesQuery = `
      WITH session_data AS (
        SELECT
          sf."sessionId",
          sf."userId",
          COALESCE(sf.language, 'unknown') as language
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false${corpClause}
      ),
      total AS (
        SELECT COUNT(*) as total_sessions FROM session_data
      )
      SELECT
        sd.language,
        COUNT(sd."sessionId") as sessions,
        COUNT(DISTINCT sd."userId") as users,
        COUNT(sd."sessionId")::float / NULLIF(t.total_sessions, 0) as percentage
      FROM session_data sd
      CROSS JOIN total t
      GROUP BY sd.language, t.total_sessions
      ORDER BY sessions DESC
      LIMIT $3
    `;

    try {
      const langParams: unknown[] = corpId === null
        ? [startDate, endDate, limit ?? 20]
        : [startDate, endDate, limit ?? 20, corpId];
      const result = await this.dataSource.query(languagesQuery, langParams);

      return result.map((row: Record<string, unknown>) => ({
        language: row.language as string,
        sessions: Number(row.sessions) || 0,
        users: Number(row.users) || 0,
        percentage: Number(row.percentage) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get languages', error);
      throw error;
    }
  }

  /**
   * Get new vs returning users over time
   */
  async getNewVsReturning(query: AudienceQueryDto): Promise<NewVsReturningMetrics[]> {
    const { startDate, endDate } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(3, corpId);

    const trendQuery = `
      WITH daily_sessions AS (
        SELECT
          DATE_TRUNC('day', sf."createdAt") as period,
          sf."userId"
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false
          AND sf."userId" IS NOT NULL${corpClause}
      ),
      user_first AS (
        SELECT "userId", MIN("createdAt") as first_session
        FROM session_fingerprints
        WHERE "userId" IS NOT NULL
        GROUP BY "userId"
      )
      SELECT
        ds.period::date as period,
        COUNT(DISTINCT CASE WHEN DATE_TRUNC('day', uf.first_session) = ds.period THEN ds."userId" END) as new_users,
        COUNT(DISTINCT CASE WHEN DATE_TRUNC('day', uf.first_session) < ds.period THEN ds."userId" END) as returning_users
      FROM daily_sessions ds
      LEFT JOIN user_first uf ON ds."userId" = uf."userId"
      GROUP BY ds.period
      ORDER BY ds.period
    `;

    try {
      const trendParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
      const result = await this.dataSource.query(trendQuery, trendParams);

      return result.map((row: Record<string, unknown>) => {
        const newUsers = Number(row.new_users) || 0;
        const returningUsers = Number(row.returning_users) || 0;
        const total = newUsers + returningUsers;

        return {
          period: (row.period as Date).toISOString().split('T')[0],
          newUsers,
          returningUsers,
          newUserRate: total > 0 ? newUsers / total : 0,
        };
      });
    } catch (error) {
      this.logger.error('Failed to get new vs returning trend', error);
      throw error;
    }
  }

  /**
   * Get network type breakdown (organization type from gov-detection).
   * Groups session_fingerprints by orgType, normalizing raw enum values to display labels.
   */
  async getNetworkType(query: AudienceQueryDto): Promise<Array<{ orgType: string; sessions: number; percentage: number }>> {
    const { startDate, endDate, limit } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(4, corpId);

    const sql = `
      WITH session_data AS (
        SELECT
          sf."sessionId",
          COALESCE(sf."orgType", 'NORMAL') as org_type
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false${corpClause}
      ),
      total AS (SELECT COUNT(*) as total_sessions FROM session_data)
      SELECT
        sd.org_type,
        COUNT(sd."sessionId") as sessions,
        COUNT(sd."sessionId")::float / NULLIF(t.total_sessions, 0) as percentage
      FROM session_data sd
      CROSS JOIN total t
      GROUP BY sd.org_type, t.total_sessions
      ORDER BY sessions DESC
      LIMIT $3
    `;

    const ORG_TYPE_LABELS: Record<string, string> = {
      NORMAL: 'Normal',
      LIBRARY: 'Library',
      EDUCATION: 'Education',
      GOVERNMENT: 'Government',
      LAW_ENFORCEMENT: 'Law Enforcement',
      MILITARY: 'Military',
      INTELLIGENCE: 'Intelligence',
      UNKNOWN: 'Unknown',
    };

    try {
      const ntParams: unknown[] = corpId === null
        ? [startDate, endDate, limit ?? 20]
        : [startDate, endDate, limit ?? 20, corpId];
      const result = await this.dataSource.query(sql, ntParams);
      return result.map((row: Record<string, unknown>) => ({
        orgType: ORG_TYPE_LABELS[String(row.org_type)] ?? String(row.org_type),
        sessions: Number(row.sessions) || 0,
        percentage: Number(row.percentage) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get network type', error);
      throw error;
    }
  }

  /**
   * Get proxy/connection type breakdown (VPN, Tor, Datacenter, Direct, etc.)
   */
  async getProxyType(query: AudienceQueryDto): Promise<Array<{ proxyType: string; sessions: number; percentage: number }>> {
    const { startDate, endDate, limit } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(4, corpId);

    const sql = `
      WITH session_data AS (
        SELECT
          sf."sessionId",
          COALESCE(sf."proxyType", 'NONE') as proxy_type
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false${corpClause}
      ),
      total AS (SELECT COUNT(*) as total_sessions FROM session_data)
      SELECT
        sd.proxy_type,
        COUNT(sd."sessionId") as sessions,
        COUNT(sd."sessionId")::float / NULLIF(t.total_sessions, 0) as percentage
      FROM session_data sd
      CROSS JOIN total t
      GROUP BY sd.proxy_type, t.total_sessions
      ORDER BY sessions DESC
      LIMIT $3
    `;

    const PROXY_TYPE_LABELS: Record<string, string> = {
      NONE: 'Direct',
      VPN: 'VPN',
      TOR: 'Tor',
      DATACENTER: 'Datacenter',
      RESIDENTIAL: 'Residential Proxy',
      PUBLIC: 'Public Proxy',
      UNKNOWN: 'Unknown',
    };

    try {
      const ptParams: unknown[] = corpId === null
        ? [startDate, endDate, limit ?? 20]
        : [startDate, endDate, limit ?? 20, corpId];
      const result = await this.dataSource.query(sql, ptParams);
      return result.map((row: Record<string, unknown>) => ({
        proxyType: PROXY_TYPE_LABELS[String(row.proxy_type)] ?? String(row.proxy_type),
        sessions: Number(row.sessions) || 0,
        percentage: Number(row.percentage) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get proxy type', error);
      throw error;
    }
  }

  /**
   * Get top ASN organizations by session count.
   */
  async getOrganizations(query: AudienceQueryDto): Promise<Array<{ org: string; sessions: number; percentage: number }>> {
    const { startDate, endDate, limit } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpSessionFilter(4, corpId);

    const sql = `
      WITH session_data AS (
        SELECT
          sf."sessionId",
          COALESCE(sf.org, 'Unknown') as org
        FROM session_fingerprints sf
        WHERE sf."createdAt" BETWEEN $1 AND $2
          AND sf."isBot" = false
          AND sf.org IS NOT NULL${corpClause}
      ),
      total AS (SELECT COUNT(*) as total_sessions FROM session_data)
      SELECT
        sd.org,
        COUNT(sd."sessionId") as sessions,
        COUNT(sd."sessionId")::float / NULLIF(t.total_sessions, 0) as percentage
      FROM session_data sd
      CROSS JOIN total t
      GROUP BY sd.org, t.total_sessions
      ORDER BY sessions DESC
      LIMIT $3
    `;

    try {
      const orgParams: unknown[] = corpId === null
        ? [startDate, endDate, limit ?? 20]
        : [startDate, endDate, limit ?? 20, corpId];
      const result = await this.dataSource.query(sql, orgParams);
      return result.map((row: Record<string, unknown>) => ({
        org: String(row.org),
        sessions: Number(row.sessions) || 0,
        percentage: Number(row.percentage) || 0,
      }));
    } catch (error) {
      this.logger.error('Failed to get organizations', error);
      throw error;
    }
  }

  /**
   * Get government traffic summary — counts and tier breakdown for the alert banner.
   */
  async getGovAlert(query: AudienceQueryDto): Promise<{
    total: number;
    hasAlert: boolean;
    breakdown: Array<{ orgType: string; responseTier: string; count: number }>;
  }> {
    const { startDate, endDate } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpId === null
      ? ''
      : ` AND "sessionId" IN (SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $3)`;

    const sql = `
      SELECT
        COALESCE("orgType", 'GOVERNMENT') as org_type,
        COALESCE("responseTier", 'SOFT_BLOCK') as response_tier,
        COUNT(*) as count
      FROM session_fingerprints
      WHERE "createdAt" BETWEEN $1 AND $2
        AND "isGovernment" = true
        AND "isBot" = false${corpClause}
      GROUP BY "orgType", "responseTier"
      ORDER BY count DESC
    `;

    const ORG_TYPE_LABELS: Record<string, string> = {
      GOVERNMENT: 'Government',
      LAW_ENFORCEMENT: 'Law Enforcement',
      MILITARY: 'Military',
      INTELLIGENCE: 'Intelligence',
      LIBRARY: 'Library',
      EDUCATION: 'Education',
      UNKNOWN: 'Unknown',
    };

    try {
      const govParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
      const result = await this.dataSource.query(sql, govParams);
      const breakdown = result.map((row: Record<string, unknown>) => ({
        orgType: ORG_TYPE_LABELS[String(row.org_type)] ?? String(row.org_type),
        responseTier: String(row.response_tier),
        count: Number(row.count) || 0,
      }));
      const total = breakdown.reduce((sum: number, r: { count: number }) => sum + r.count, 0);
      const hasAlert = breakdown.some((r: { responseTier: string }) =>
        r.responseTier === 'HARD_BLOCK' || r.responseTier === 'ALERT',
      );
      return { total, hasAlert, breakdown };
    } catch (error) {
      this.logger.error('Failed to get gov alert', error);
      return { total: 0, hasAlert: false, breakdown: [] };
    }
  }

  /**
   * Get IP classification list — sessions grouped by org/ASN with their detection signals.
   * Returns top 100 groups ordered by session count descending.
   */
  async getIPClassifications(query: AudienceQueryDto): Promise<Array<{
    org: string | null;
    asn: number | null;
    orgType: string | null;
    proxyType: string | null;
    responseTier: string | null;
    country: string | null;
    sessions: number;
    firstSeen: string;
    lastSeen: string;
  }>> {
    const { startDate, endDate } = query;
    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpId === null
      ? ''
      : ` AND "sessionId" IN (SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $3)`;

    const sql = `
      SELECT
        "org",
        "asn",
        COALESCE("orgType", 'NORMAL') as org_type,
        COALESCE("proxyType", 'NONE') as proxy_type,
        "responseTier" as response_tier,
        "country",
        COUNT(*) as sessions,
        MIN("createdAt") as first_seen,
        MAX("createdAt") as last_seen
      FROM session_fingerprints
      WHERE "createdAt" BETWEEN $1 AND $2
        AND "isBot" = false
        AND (
          "org" IS NOT NULL
          OR "asn" IS NOT NULL
          OR "orgType" NOT IN ('NORMAL', 'UNKNOWN')
          OR "proxyType" NOT IN ('NONE', 'UNKNOWN')
        )${corpClause}
      GROUP BY "org", "asn", "orgType", "proxyType", "responseTier", "country"
      ORDER BY sessions DESC
      LIMIT 100
    `;

    try {
      const ipParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
      const result = await this.dataSource.query(sql, ipParams);
      return result.map((row: Record<string, unknown>) => ({
        org: row.org ? String(row.org) : null,
        asn: row.asn ? Number(row.asn) : null,
        orgType: row.org_type ? String(row.org_type) : null,
        proxyType: row.proxy_type ? String(row.proxy_type) : null,
        responseTier: row.response_tier ? String(row.response_tier) : null,
        country: row.country ? String(row.country) : null,
        sessions: Number(row.sessions) || 0,
        firstSeen: row.first_seen instanceof Date
          ? row.first_seen.toISOString()
          : String(row.first_seen),
        lastSeen: row.last_seen instanceof Date
          ? row.last_seen.toISOString()
          : String(row.last_seen),
      }));
    } catch (error) {
      this.logger.error('Failed to get IP classifications', error);
      return [];
    }
  }

  private getGeoColumn(granularity?: GeoGranularity): string {
    switch (granularity) {
      case GeoGranularity.REGION:
        return 'sf.region';
      case GeoGranularity.CITY:
        return 'sf.city';
      case GeoGranularity.COUNTRY:
      default:
        return 'sf.country';
    }
  }
}
