import {
  TABLE_NAMES,
  ch,
  clix,
  eventBuffer,
  getChartPrevStartEndDate,
  getChartStartEndDate,
  getOrganizationSubscriptionChartEndDate,
  getSettingsForProject,
  overviewService,
  zGetMetricsInput,
  zGetTopGenericInput,
  zGetTopPagesInput,
} from '@openpanel/db';
import { type IChartRange, zRange } from '@openpanel/validation';
import { format } from 'date-fns';
import { z } from 'zod';
import { cacheMiddleware, createTRPCRouter, publicProcedure } from '../trpc';

const cacher = cacheMiddleware((input, opts) => {
  const range = input.range as IChartRange;
  if (opts.path === 'overview.liveData') {
    return 0;
  }

  switch (range) {
    case '30min':
    case 'today':
    case 'lastHour':
      return 1;
    default:
      return 1;
  }
});

function getCurrentAndPrevious<
  T extends {
    startDate?: string | null;
    endDate?: string | null;
    range: IChartRange;
    projectId: string;
  },
>(input: T, fetchPrevious: boolean, timezone: string) {
  const current = getChartStartEndDate(input, timezone);
  const previous = getChartPrevStartEndDate(current);

  return async <R>(
    fn: (input: T & { startDate: string; endDate: string }) => Promise<R>,
  ): Promise<{
    current: R;
    previous: R | null;
  }> => {
    const endDate = await getOrganizationSubscriptionChartEndDate(
      input.projectId,
      current.endDate,
    );
    if (endDate) {
      current.endDate = endDate;
      // Only expired trial scenarios
      if (new Date(current.startDate) > new Date(current.endDate)) {
        current.startDate = current.endDate;
      }
    }
    const res = await Promise.all([
      fn({
        ...input,
        startDate: current.startDate,
        endDate: current.endDate,
      }),
      fetchPrevious
        ? fn({
            ...input,
            startDate: previous.startDate,
            endDate: previous.endDate,
          })
        : Promise.resolve(null),
    ]);

    return {
      current: res[0],
      previous: res[1],
    };
  };
}

export const overviewRouter = createTRPCRouter({
  liveVisitors: publicProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ input }) => {
      return eventBuffer.getActiveVisitorCount(input.projectId);
    }),

  liveData: publicProcedure
    .input(z.object({ projectId: z.string() }))
    .use(cacher)
    .query(async ({ input }) => {
      const { timezone } = await getSettingsForProject(input.projectId);

      // Get total unique sessions in the last 30 minutes
      const totalSessionsQuery = clix(ch, timezone)
        .select<{ total_sessions: number }>([
          'uniq(session_id) as total_sessions',
        ])
        .from(TABLE_NAMES.events)
        .where('project_id', '=', input.projectId)
        .where('created_at', '>=', clix.exp('now() - INTERVAL 30 MINUTE'));

      // Get counts per minute for the last 30 minutes
      const minuteCountsQuery = clix(ch, timezone)
        .select<{
          minute: string;
          session_count: number;
          visitor_count: number;
        }>([
          `${clix.toStartOf('created_at', 'minute')} as minute`,
          'uniq(session_id) as session_count',
          'uniq(profile_id) as visitor_count',
        ])
        .from(TABLE_NAMES.events)
        .where('project_id', '=', input.projectId)
        .where('created_at', '>=', clix.exp('now() - INTERVAL 30 MINUTE'))
        .groupBy(['minute'])
        .orderBy('minute', 'ASC')
        .fill(
          clix.exp('toStartOfMinute(now() - INTERVAL 30 MINUTE)'),
          clix.exp('toStartOfMinute(now())'),
          clix.exp('INTERVAL 1 MINUTE'),
        );

      // Get referrers per minute for the last 30 minutes
      const minuteReferrersQuery = clix(ch, timezone)
        .select<{
          minute: string;
          referrer_name: string;
          count: number;
        }>([
          `${clix.toStartOf('created_at', 'minute')} as minute`,
          'referrer_name',
          'uniq(session_id) as count',
        ])
        .from(TABLE_NAMES.events)
        .where('project_id', '=', input.projectId)
        .where('created_at', '>=', clix.exp('now() - INTERVAL 30 MINUTE'))
        .where('referrer_name', '!=', '')
        .where('referrer_name', 'IS NOT NULL')
        .groupBy(['minute', 'referrer_name'])
        .orderBy('minute', 'ASC')
        .orderBy('count', 'DESC');

      // Get unique referrers in the last 30 minutes
      const referrersQuery = clix(ch, timezone)
        .select<{ referrer: string; count: number }>([
          'referrer_name as referrer',
          'uniq(session_id) as count',
        ])
        .from(TABLE_NAMES.events)
        .where('project_id', '=', input.projectId)
        .where('created_at', '>=', clix.exp('now() - INTERVAL 30 MINUTE'))
        .where('referrer_name', '!=', '')
        .where('referrer_name', 'IS NOT NULL')
        .groupBy(['referrer_name'])
        .orderBy('count', 'DESC')
        .limit(10);

      const [totalSessions, minuteCounts, minuteReferrers, referrers] =
        await Promise.all([
          totalSessionsQuery.execute(),
          minuteCountsQuery.execute(),
          minuteReferrersQuery.execute(),
          referrersQuery.execute(),
        ]);

      // Group referrers by minute
      const referrersByMinute = new Map<
        string,
        Array<{ referrer: string; count: number }>
      >();
      minuteReferrers.forEach((item) => {
        if (!referrersByMinute.has(item.minute)) {
          referrersByMinute.set(item.minute, []);
        }
        referrersByMinute.get(item.minute)!.push({
          referrer: item.referrer_name,
          count: item.count,
        });
      });

      return {
        totalSessions: totalSessions[0]?.total_sessions || 0,
        minuteCounts: minuteCounts.map((item) => ({
          minute: item.minute,
          sessionCount: item.session_count,
          visitorCount: item.visitor_count,
          timestamp: new Date(item.minute).getTime(),
          time: new Date(item.minute).toLocaleTimeString([], {
            hour: '2-digit',
            minute: '2-digit',
          }),
          referrers: referrersByMinute.get(item.minute) || [],
        })),
        referrers: referrers.map((item) => ({
          referrer: item.referrer,
          count: item.count,
        })),
      };
    }),
  stats: publicProcedure
    .input(
      zGetMetricsInput.omit({ startDate: true, endDate: true }).extend({
        startDate: z.string().nullish(),
        endDate: z.string().nullish(),
        range: zRange,
      }),
    )
    .use(cacher)
    .query(async ({ ctx, input }) => {
      const { timezone } = await getSettingsForProject(input.projectId);
      const { current, previous } = await getCurrentAndPrevious(
        { ...input, timezone },
        true,
        timezone,
      )(overviewService.getMetrics.bind(overviewService));
      return {
        metrics: {
          ...current.metrics,
          prev_bounce_rate: previous?.metrics.bounce_rate || null,
          prev_unique_visitors: previous?.metrics.unique_visitors || null,
          prev_total_screen_views: previous?.metrics.total_screen_views || null,
          prev_avg_session_duration:
            previous?.metrics.avg_session_duration || null,
          prev_views_per_session: previous?.metrics.views_per_session || null,
          prev_total_sessions: previous?.metrics.total_sessions || null,
          prev_total_revenue: previous?.metrics.total_revenue || null,
        },
        series: current.series.map((item, index) => {
          const prev = previous?.series[index];
          return {
            ...item,
            date: format(item.date, 'yyyy-MM-dd HH:mm:ss'),
            prev_bounce_rate: prev?.bounce_rate,
            prev_unique_visitors: prev?.unique_visitors,
            prev_total_screen_views: prev?.total_screen_views,
            prev_avg_session_duration: prev?.avg_session_duration,
            prev_views_per_session: prev?.views_per_session,
            prev_total_sessions: prev?.total_sessions,
            prev_total_revenue: prev?.total_revenue,
          };
        }),
      };
    }),

  topPages: publicProcedure
    .input(
      zGetTopPagesInput.omit({ startDate: true, endDate: true }).extend({
        startDate: z.string().nullish(),
        endDate: z.string().nullish(),
        range: zRange,
        mode: z.enum(['page', 'entry', 'exit', 'bot']),
      }),
    )
    .use(cacher)
    .query(async ({ input }) => {
      const { timezone } = await getSettingsForProject(input.projectId);
      const { current } = await getCurrentAndPrevious(
        { ...input },
        false,
        timezone,
      )(async (input) => {
        if (input.mode === 'page') {
          return overviewService.getTopPages({ ...input, timezone });
        }

        if (input.mode === 'bot') {
          return Promise.resolve([]);
        }

        return overviewService.getTopEntryExit({
          ...input,
          mode: input.mode,
          timezone,
        });
      });

      return current;
    }),

  topGeneric: publicProcedure
    .input(
      zGetTopGenericInput.omit({ startDate: true, endDate: true }).extend({
        startDate: z.string().nullish(),
        endDate: z.string().nullish(),
        range: zRange,
      }),
    )
    .use(cacher)
    .query(async ({ input }) => {
      const { timezone } = await getSettingsForProject(input.projectId);
      const { current } = await getCurrentAndPrevious(
        { ...input, timezone },
        false,
        timezone,
      )(overviewService.getTopGeneric.bind(overviewService));

      return current;
    }),
});
