import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Between, DataSource, Repository } from 'typeorm';
import { AggregatedMetric, MetricType, TimeGranularity } from '../entities/aggregated-metric.entity';
import { TrendsQueryDto } from './dto/trends-query.dto';
import { corpRawEventsFilter, resolveCorpId } from '../common/corp-filter.util';

export interface TrendDataPoint {
  timestamp: Date;
  value: number;
  count: number;
}

export interface TrendsResult {
  metric: string;
  granularity: string;
  data: TrendDataPoint[];
  summary: {
    total: number;
    average: number;
    min: number;
    max: number;
  };
}

export interface TrendsByCorpSeries {
  corpId: number;
  corpSlug: string;
  data: Array<{ timestamp: Date; value: number }>;
}

export interface TrendsByCorpResult {
  metric: string;
  granularity: string;
  byCorp: TrendsByCorpSeries[];
}

const VALID_GRANULARITIES = new Set<string>(Object.values(TimeGranularity));

@Injectable()
export class TrendsService {
  constructor(
    @InjectRepository(AggregatedMetric)
    private readonly metricsRepository: Repository<AggregatedMetric>,
    @InjectDataSource()
    private readonly dataSource: DataSource,
  ) {}

  async getTrends(query: TrendsQueryDto): Promise<TrendsResult | TrendsByCorpResult> {
    if (query.groupBy === 'corp') {
      return this.getTrendsByCorp(query);
    }
    const { metric, startDate, endDate, granularity = 'day' } = query;

    const safeBucket = VALID_GRANULARITIES.has(granularity) ? granularity : 'day';
    const bucketExpr = `date_trunc('${safeBucket}', timestamp AT TIME ZONE 'UTC')`;

    const corpId = await resolveCorpId(this.dataSource, query.corp);
    const corpClause = corpRawEventsFilter(3, corpId);
    const baseParams: Array<string | number> = [startDate, endDate];
    const queryParams = corpId === null ? baseParams : [...baseParams, corpId];

    let rawSql: string | null = null;

    if (metric === MetricType.PAGE_VIEWS) {
      rawSql = `
        SELECT ${bucketExpr} AS timestamp, COUNT(*)::bigint AS value
        FROM raw_events
        WHERE "eventType" IN ('pageview', 'pageView')
          AND timestamp BETWEEN $1 AND $2${corpClause}
        GROUP BY ${bucketExpr}
        ORDER BY timestamp ASC
      `;
    } else if (metric === MetricType.SESSIONS) {
      rawSql = `
        SELECT ${bucketExpr} AS timestamp, COUNT(DISTINCT "sessionId")::bigint AS value
        FROM raw_events
        WHERE timestamp BETWEEN $1 AND $2${corpClause}
        GROUP BY ${bucketExpr}
        ORDER BY timestamp ASC
      `;
    } else if (metric === MetricType.UNIQUE_VISITORS) {
      rawSql = `
        SELECT ${bucketExpr} AS timestamp,
               COUNT(DISTINCT COALESCE("userId", "sessionId"))::bigint AS value
        FROM raw_events
        WHERE timestamp BETWEEN $1 AND $2${corpClause}
        GROUP BY ${bucketExpr}
        ORDER BY timestamp ASC
      `;
    }

    if (rawSql) {
      const rows = (await this.metricsRepository.query(rawSql, queryParams)) as Array<{
        timestamp: Date;
        value: string;
      }>;
      const values = rows.map((r) => Number(r.value));
      const total = values.reduce((sum, v) => sum + v, 0);
      return {
        metric,
        granularity: safeBucket,
        data: rows.map((r) => ({ timestamp: r.timestamp, value: Number(r.value), count: Number(r.value) })),
        summary: {
          total,
          average: values.length > 0 ? total / values.length : 0,
          min: values.length > 0 ? Math.min(...values) : 0,
          max: values.length > 0 ? Math.max(...values) : 0,
        },
      };
    }

    // Fallback: pre-aggregated table for other metrics. aggregated_metrics has
    // no corp dimension yet, so corp-scoped queries silently return the un-scoped
    // aggregate. Acceptable v1 — funnels/conversion etc. are not the corp focus.
    const data = await this.metricsRepository.find({
      where: {
        metricType: metric as MetricType,
        granularity: safeBucket as TimeGranularity,
        timestamp: Between(new Date(startDate), new Date(endDate)),
      },
      order: { timestamp: 'ASC' },
    });

    const values = data.map((d) => Number(d.value));
    const total = values.reduce((sum, v) => sum + v, 0);

    return {
      metric,
      granularity: safeBucket,
      data: data.map((d) => ({
        timestamp: d.timestamp,
        value: Number(d.value),
        count: Number(d.count),
      })),
      summary: {
        total,
        average: values.length > 0 ? total / values.length : 0,
        min: values.length > 0 ? Math.min(...values) : 0,
        max: values.length > 0 ? Math.max(...values) : 0,
      },
    };
  }

  async compareTrends(
    query: TrendsQueryDto,
    compareStartDate: string,
    compareEndDate: string,
  ): Promise<{ current: TrendsResult; previous: TrendsResult; change: number }> {
    if (query.groupBy === 'corp') {
      throw new BadRequestException('compareTrends does not support groupBy=corp');
    }
    const baseQuery: TrendsQueryDto = { ...query, groupBy: undefined };
    const current = await this.getTrendsUngrouped(baseQuery);
    const previous = await this.getTrendsUngrouped({
      ...baseQuery,
      startDate: compareStartDate,
      endDate: compareEndDate,
    });

    const currentTotal = current.summary.total;
    const previousTotal = previous.summary.total;
    const change =
      previousTotal > 0 ? ((currentTotal - previousTotal) / previousTotal) * 100 : 0;

    return { current, previous, change };
  }


  private async getTrendsUngrouped(query: TrendsQueryDto): Promise<TrendsResult> {
    const result = await this.getTrends({ ...query, groupBy: undefined });
    if ('byCorp' in result) {
      // Unreachable: groupBy is forced undefined above.
      throw new BadRequestException('Unexpected grouped result');
    }
    return result;
  }

  private async getTrendsByCorp(query: TrendsQueryDto): Promise<TrendsByCorpResult> {
    const { metric, startDate, endDate, granularity = 'day' } = query;
    const safeBucket = VALID_GRANULARITIES.has(granularity) ? granularity : 'day';
    const bucketExpr = `date_trunc('${safeBucket}', re.timestamp AT TIME ZONE 'UTC')`;

    let valueExpr: string | null = null;
    if (metric === MetricType.PAGE_VIEWS) {
      valueExpr = `COUNT(*) FILTER (WHERE re."eventType" IN ('pageview', 'pageView'))::bigint`;
    } else if (metric === MetricType.SESSIONS) {
      valueExpr = `COUNT(DISTINCT re."sessionId")::bigint`;
    } else if (metric === MetricType.UNIQUE_VISITORS) {
      valueExpr = `COUNT(DISTINCT COALESCE(re."userId", re."sessionId"))::bigint`;
    } else {
      throw new BadRequestException(
        `metric=${metric} is not supported with groupBy=corp; use page_views, sessions, or unique_visitors`,
      );
    }

    const sql = `
      SELECT
        c.id AS corp_id,
        c.slug AS corp_slug,
        ${bucketExpr} AS timestamp,
        ${valueExpr} AS value
      FROM raw_events re
      INNER JOIN corps c ON c.id = re.corp_id
      WHERE re.timestamp BETWEEN $1 AND $2
      GROUP BY c.id, c.slug, ${bucketExpr}
      ORDER BY c.slug ASC, timestamp ASC
    `;

    const rows = (await this.metricsRepository.query(sql, [startDate, endDate])) as Array<{
      corp_id: number;
      corp_slug: string;
      timestamp: Date;
      value: string;
    }>;

    const byCorpMap = new Map<number, TrendsByCorpSeries>();
    for (const row of rows) {
      const corpId = Number(row.corp_id);
      let series = byCorpMap.get(corpId);
      if (!series) {
        series = { corpId, corpSlug: row.corp_slug, data: [] };
        byCorpMap.set(corpId, series);
      }
      series.data.push({ timestamp: row.timestamp, value: Number(row.value) });
    }

    return {
      metric,
      granularity: safeBucket,
      byCorp: Array.from(byCorpMap.values()),
    };
  }
}
