import os from 'os';
import fs from 'fs';
import { prisma } from '../../config/database';
import { dockerService } from '../../services/docker.service';
import { mapSettingsService } from '../map/settings/settings.service';
import { env } from '../../config/env';
import { fetchWithTimeout } from '../../utils/fetch-with-timeout';
import { validatePromQLQueries } from '../../utils/promql-validator';
import { isServiceOnline } from '../../utils/health-check';
import { listmonkClient } from '../../services/listmonk.client';
import { gancioClient } from '../../services/gancio.client';
import { rocketchatClient } from '../../services/rocketchat.client';
import { giteaClient } from '../../services/gitea.client';
import { vaultwardenClient } from '../../services/vaultwarden.client';
import { emailService } from '../../services/email.service';
import { redis } from '../../config/redis';
import { logger } from '../../utils/logger';

// --- Types ---

export interface DashboardSummary {
  users: {
    total: number;
    byRole: Record<string, number>;
    active: number;
    suspended: number;
  };
  campaigns: {
    total: number;
    active: number;
    draft: number;
    paused: number;
    archived: number;
  };
  locations: {
    total: number;
    geocoded: number;
    addresses: number;
  };
  emails: {
    total: number;
    sent: number;
    failed: number;
    queued: number;
  };
  shifts: {
    total: number;
    open: number;
    upcoming: number;
  };
  canvass: {
    totalSessions: number;
    totalVisits: number;
    activeSessions: number;
  };
  responses: {
    total: number;
    pending: number;
    approved: number;
  };
  videos: {
    total: number;
    published: number;
  };
  pages: {
    total: number;
    published: number;
  };
  emailTemplates: {
    total: number;
  };
  cuts: {
    total: number;
  };
  representatives: {
    totalCached: number;
  };
  campaignModeration: {
    pendingReview: number;
  };
  docsComments: {
    pending: number;
  };
}

export interface SystemInfo {
  hostname: string;
  platform: string;
  arch: string;
  nodeVersion: string;
  uptime: number; // seconds
  cpu: {
    model: string;
    cores: number;
    loadAvg: number[]; // 1, 5, 15 min
  };
  memory: {
    totalMB: number;
    usedMB: number;
    freeMB: number;
    usagePercent: number;
  };
  disk: {
    totalGB: number;
    usedGB: number;
    freeGB: number;
    usagePercent: number;
  } | null;
  process: {
    heapUsedMB: number;
    heapTotalMB: number;
    rssMB: number;
    uptimeSeconds: number;
  };
}

export interface ContainerInfo {
  name: string;
  label: string;
  running: boolean;
  status: string;
}

export interface WeatherData {
  latitude: number;
  longitude: number;
  temperature: number;
  apparentTemperature: number;
  humidity: number;
  windSpeed: number;
  windDirection: number;
  weatherCode: number;
  weatherDescription: string;
  isDay: boolean;
  precipitation: number;
  cloudCover: number;
  time: string;
}

// Container list is now auto-discovered from Docker network (no hardcoded names needed).
// See dockerService.listNetworkContainers() — queries Docker API for all containers
// on env.DOCKER_NETWORK_NAME and derives labels from compose service names.

// --- WMO weather code descriptions ---

const WMO_CODES: Record<number, string> = {
  0: 'Clear sky',
  1: 'Mainly clear',
  2: 'Partly cloudy',
  3: 'Overcast',
  45: 'Fog',
  48: 'Depositing rime fog',
  51: 'Light drizzle',
  53: 'Moderate drizzle',
  55: 'Dense drizzle',
  56: 'Light freezing drizzle',
  57: 'Dense freezing drizzle',
  61: 'Slight rain',
  63: 'Moderate rain',
  65: 'Heavy rain',
  66: 'Light freezing rain',
  67: 'Heavy freezing rain',
  71: 'Slight snowfall',
  73: 'Moderate snowfall',
  75: 'Heavy snowfall',
  77: 'Snow grains',
  80: 'Slight rain showers',
  81: 'Moderate rain showers',
  82: 'Violent rain showers',
  85: 'Slight snow showers',
  86: 'Heavy snow showers',
  95: 'Thunderstorm',
  96: 'Thunderstorm with slight hail',
  99: 'Thunderstorm with heavy hail',
};

// --- Service functions ---

export async function getDashboardSummary(): Promise<DashboardSummary> {
  const now = new Date();

  const [
    usersTotal, usersSuperAdmin, usersInfluenceAdmin, usersMapAdmin, usersUser, usersTemp,
    usersActive, usersSuspended,
    campaignsTotal, campaignsActive, campaignsDraft, campaignsPaused, campaignsArchived,
    locationsTotal, locationsGeocoded, addressesTotal,
    emailsTotal, emailsSent, emailsFailed, emailsQueued,
    shiftsTotal, shiftsOpen, shiftsUpcoming,
    canvassSessions, canvassVisits, canvassActive,
    responsesTotal, responsesPending, responsesApproved,
    videosTotal, videosPublished,
    pagesTotal, pagesPublished,
    emailTemplatesTotal,
    cutsTotal,
    repsCached,
    campaignsPendingReview,
    docsCommentsPending,
  ] = await Promise.all([
    prisma.user.count(),
    prisma.user.count({ where: { role: 'SUPER_ADMIN' } }),
    prisma.user.count({ where: { role: 'INFLUENCE_ADMIN' } }),
    prisma.user.count({ where: { role: 'MAP_ADMIN' } }),
    prisma.user.count({ where: { role: 'USER' } }),
    prisma.user.count({ where: { role: 'TEMP' } }),
    prisma.user.count({ where: { status: 'ACTIVE' } }),
    prisma.user.count({ where: { status: 'SUSPENDED' } }),
    prisma.campaign.count(),
    prisma.campaign.count({ where: { status: 'ACTIVE' } }),
    prisma.campaign.count({ where: { status: 'DRAFT' } }),
    prisma.campaign.count({ where: { status: 'PAUSED' } }),
    prisma.campaign.count({ where: { status: 'ARCHIVED' } }),
    prisma.location.count(),
    prisma.location.count({ where: { geocodeProvider: { not: null } } }),
    prisma.address.count(),
    prisma.campaignEmail.count(),
    prisma.campaignEmail.count({ where: { status: 'SENT' } }),
    prisma.campaignEmail.count({ where: { status: 'FAILED' } }),
    prisma.campaignEmail.count({ where: { status: 'QUEUED' } }),
    prisma.shift.count(),
    prisma.shift.count({ where: { status: 'OPEN' } }),
    prisma.shift.count({ where: { date: { gte: now }, status: { not: 'CANCELLED' } } }),
    prisma.canvassSession.count(),
    prisma.canvassVisit.count(),
    prisma.canvassSession.count({ where: { status: 'ACTIVE' } }),
    prisma.representativeResponse.count(),
    prisma.representativeResponse.count({ where: { status: 'PENDING' } }),
    prisma.representativeResponse.count({ where: { status: 'APPROVED' } }),
    prisma.video.count(),
    prisma.video.count({ where: { isPublished: true } }),
    prisma.landingPage.count(),
    prisma.landingPage.count({ where: { published: true } }),
    prisma.emailTemplate.count(),
    prisma.cut.count(),
    prisma.representative.count(),
    prisma.campaign.count({ where: { moderationStatus: 'PENDING_REVIEW' } }),
    prisma.docsComment.count({ where: { status: 'PENDING' } }).catch(() => 0),
  ]);

  return {
    users: {
      total: usersTotal,
      byRole: {
        SUPER_ADMIN: usersSuperAdmin, INFLUENCE_ADMIN: usersInfluenceAdmin,
        MAP_ADMIN: usersMapAdmin, USER: usersUser, TEMP: usersTemp,
      },
      active: usersActive, suspended: usersSuspended,
    },
    campaigns: { total: campaignsTotal, active: campaignsActive, draft: campaignsDraft, paused: campaignsPaused, archived: campaignsArchived },
    locations: { total: locationsTotal, geocoded: locationsGeocoded, addresses: addressesTotal },
    emails: { total: emailsTotal, sent: emailsSent, failed: emailsFailed, queued: emailsQueued },
    shifts: { total: shiftsTotal, open: shiftsOpen, upcoming: shiftsUpcoming },
    canvass: { totalSessions: canvassSessions, totalVisits: canvassVisits, activeSessions: canvassActive },
    responses: { total: responsesTotal, pending: responsesPending, approved: responsesApproved },
    videos: { total: videosTotal, published: videosPublished },
    pages: { total: pagesTotal, published: pagesPublished },
    emailTemplates: { total: emailTemplatesTotal },
    cuts: { total: cutsTotal },
    representatives: { totalCached: repsCached },
    campaignModeration: { pendingReview: campaignsPendingReview },
    docsComments: { pending: docsCommentsPending },
  };
}

// --- Activity Feed ---

export interface ActivityItem {
  id: string;
  type: 'shift_signup' | 'response_submitted' | 'canvass_completed' | 'email_sent' | 'user_created';
  module: 'map' | 'influence' | 'users';
  title: string;
  description: string;
  timestamp: string;
}

export interface ActivityFeedResult {
  items: ActivityItem[];
  total: number;
  page: number;
  limit: number;
}

export async function getActivityFeed(opts: {
  page: number;
  limit: number;
  module?: string;
}): Promise<ActivityFeedResult> {
  const { page, limit, module } = opts;
  const skip = (page - 1) * limit;
  const since = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // last 7 days

  const items: ActivityItem[] = [];

  const queries: Promise<void>[] = [];

  // Shift signups (map module)
  if (!module || module === 'all' || module === 'map') {
    queries.push(
      prisma.shiftSignup.findMany({
        where: { signupDate: { gte: since }, status: 'CONFIRMED' },
        orderBy: { signupDate: 'desc' },
        take: limit * 2,
        select: { id: true, userName: true, userEmail: true, shiftTitle: true, signupDate: true },
      }).then(signups => {
        for (const s of signups) {
          items.push({
            id: `signup-${s.id}`,
            type: 'shift_signup',
            module: 'map',
            title: 'Shift Signup',
            description: `${s.userName || s.userEmail} signed up for ${s.shiftTitle}`,
            timestamp: s.signupDate.toISOString(),
          });
        }
      }).catch(() => {}),
    );
  }

  // Canvass sessions (map module)
  if (!module || module === 'all' || module === 'map') {
    queries.push(
      prisma.canvassSession.findMany({
        where: { status: 'COMPLETED', endedAt: { gte: since } },
        orderBy: { endedAt: 'desc' },
        take: limit * 2,
        select: { id: true, endedAt: true, userId: true, cut: { select: { name: true } } },
      }).then(async sessions => {
        const userIds = sessions.map(s => s.userId);
        const users = await prisma.user.findMany({
          where: { id: { in: userIds } },
          select: { id: true, name: true, email: true },
        });
        const userMap = new Map(users.map(u => [u.id, u]));
        for (const s of sessions) {
          const user = userMap.get(s.userId);
          items.push({
            id: `canvass-${s.id}`,
            type: 'canvass_completed',
            module: 'map',
            title: 'Canvass Completed',
            description: `${user?.name || user?.email || 'Unknown'} completed a session${s.cut?.name ? ` in ${s.cut.name}` : ''}`,
            timestamp: (s.endedAt || new Date()).toISOString(),
          });
        }
      }).catch(() => {}),
    );
  }

  // Responses (influence module)
  if (!module || module === 'all' || module === 'influence') {
    queries.push(
      prisma.representativeResponse.findMany({
        where: { createdAt: { gte: since } },
        orderBy: { createdAt: 'desc' },
        take: limit * 2,
        select: { id: true, createdAt: true, submittedByName: true, campaign: { select: { title: true } } },
      }).then(responses => {
        for (const r of responses) {
          items.push({
            id: `response-${r.id}`,
            type: 'response_submitted',
            module: 'influence',
            title: 'Response Submitted',
            description: `${r.submittedByName || 'Anonymous'} submitted a response for ${r.campaign.title}`,
            timestamp: r.createdAt.toISOString(),
          });
        }
      }).catch(() => {}),
    );
  }

  // Users (users module)
  if (!module || module === 'all' || module === 'users') {
    queries.push(
      prisma.user.findMany({
        where: { createdAt: { gte: since } },
        orderBy: { createdAt: 'desc' },
        take: limit * 2,
        select: { id: true, name: true, email: true, role: true, createdAt: true },
      }).then(users => {
        for (const u of users) {
          items.push({
            id: `user-${u.id}`,
            type: 'user_created',
            module: 'users',
            title: 'New User',
            description: `${u.name || u.email} (${u.role.replace(/_/g, ' ')})`,
            timestamp: u.createdAt.toISOString(),
          });
        }
      }).catch(() => {}),
    );
  }

  await Promise.all(queries);

  // Sort by timestamp descending, paginate
  items.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
  const total = items.length;
  const paged = items.slice(skip, skip + limit);

  return { items: paged, total, page, limit };
}

// --- Connectivity Checks ---

export interface ConnectivityStatus {
  smtp: boolean;
  listmonk: boolean;
  rocketchat: boolean;
  gancio: boolean;
}

export async function getConnectivity(): Promise<ConnectivityStatus> {
  const [smtp, listmonk, rocketchat, gancio] = await Promise.all([
    emailService.testConnection().catch(() => false),
    listmonkClient.checkHealth().catch(() => false),
    isServiceOnline(env.ROCKETCHAT_URL || '', 3000).catch(() => false),
    gancioClient.isAvailable().catch(() => false),
  ]);

  return {
    smtp: !!smtp,
    listmonk: !!listmonk,
    rocketchat: !!rocketchat,
    gancio: !!gancio,
  };
}

export function getSystemInfo(): SystemInfo {
  const totalMem = os.totalmem();
  const freeMem = os.freemem();
  const usedMem = totalMem - freeMem;
  const cpus = os.cpus();
  const mem = process.memoryUsage();

  let disk: SystemInfo['disk'] = null;
  try {
    const stats = fs.statfsSync('/');
    const totalBytes = stats.bsize * stats.blocks;
    const freeBytes = stats.bsize * stats.bavail;
    const usedBytes = totalBytes - freeBytes;
    disk = {
      totalGB: Math.round((totalBytes / 1073741824) * 10) / 10,
      usedGB: Math.round((usedBytes / 1073741824) * 10) / 10,
      freeGB: Math.round((freeBytes / 1073741824) * 10) / 10,
      usagePercent: Math.round((usedBytes / totalBytes) * 100),
    };
  } catch {
    // statfsSync may not be available on all platforms
  }

  return {
    hostname: os.hostname(),
    platform: `${os.type()} ${os.release()}`,
    arch: os.arch(),
    nodeVersion: process.version,
    uptime: Math.floor(os.uptime()),
    cpu: {
      model: cpus[0]?.model?.trim() || 'Unknown',
      cores: cpus.length,
      loadAvg: os.loadavg().map(v => Math.round(v * 100) / 100),
    },
    memory: {
      totalMB: Math.round(totalMem / 1048576),
      usedMB: Math.round(usedMem / 1048576),
      freeMB: Math.round(freeMem / 1048576),
      usagePercent: Math.round((usedMem / totalMem) * 100),
    },
    disk,
    process: {
      heapUsedMB: Math.round(mem.heapUsed / 1048576),
      heapTotalMB: Math.round(mem.heapTotal / 1048576),
      rssMB: Math.round(mem.rss / 1048576),
      uptimeSeconds: Math.floor(process.uptime()),
    },
  };
}

export async function getContainerStatuses(): Promise<ContainerInfo[]> {
  // Auto-discover containers from Docker network — no hardcoded list needed
  return dockerService.listNetworkContainers();
}

export async function getWeather(): Promise<WeatherData | null> {
  try {
    const settings = await mapSettingsService.get();
    const lat = settings.latitude ? Number(settings.latitude) : null;
    const lng = settings.longitude ? Number(settings.longitude) : null;

    if (!lat || !lng) return null;

    const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,cloud_cover,wind_speed_10m,wind_direction_10m,is_day&temperature_unit=celsius&wind_speed_unit=kmh`;

    const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
    if (!response.ok) return null;

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const data = await response.json() as { current: Record<string, any> };
    const current = data.current;

    return {
      latitude: lat,
      longitude: lng,
      temperature: current.temperature_2m,
      apparentTemperature: current.apparent_temperature,
      humidity: current.relative_humidity_2m,
      windSpeed: current.wind_speed_10m,
      windDirection: current.wind_direction_10m,
      weatherCode: current.weather_code,
      weatherDescription: WMO_CODES[current.weather_code] || 'Unknown',
      isDay: current.is_day === 1,
      precipitation: current.precipitation,
      cloudCover: current.cloud_cover,
      time: current.time,
    };
  } catch (err) {
    logger.warn('Failed to fetch weather data', err);
    return null;
  }
}

// --- Time-Series from Prometheus ---

/** Predefined metric key → PromQL mapping (prevents PromQL injection) */
const METRIC_QUERIES: Record<string, string> = {
  request_rate_2xx: 'sum(rate(http_requests_total{status_code=~"2.."}[5m]))',
  request_rate_4xx: 'sum(rate(http_requests_total{status_code=~"4.."}[5m]))',
  request_rate_5xx: 'sum(rate(http_requests_total{status_code=~"5.."}[5m]))',
  latency_p50: 'histogram_quantile(0.50, sum by(le) (rate(http_request_duration_seconds_bucket[5m])))',
  latency_p95: 'histogram_quantile(0.95, sum by(le) (rate(http_request_duration_seconds_bucket[5m])))',
  latency_p99: 'histogram_quantile(0.99, sum by(le) (rate(http_request_duration_seconds_bucket[5m])))',
  email_sent_rate: 'rate(cm_emails_sent_total[5m])',
  email_failed_rate: 'rate(cm_emails_failed_total[5m])',
  email_queue_size: 'cm_email_queue_size',
  cpu_usage: '100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)',
  memory_usage: '(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100',
  active_sessions: 'cm_active_sessions',
  login_rate: 'rate(cm_login_attempts_total[5m])',
};

const ALLOWED_RANGES: Record<string, number> = { '1h': 3600, '6h': 21600, '24h': 86400 };
const ALLOWED_STEPS: Record<string, string> = { '1m': '60', '5m': '300', '15m': '900' };

export interface TimeSeriesPoint {
  timestamps: number[];
  values: number[];
}

export type TimeSeriesResult = Record<string, TimeSeriesPoint>;

export async function getTimeSeries(
  metricKeys: string[],
  range: string,
  step: string,
): Promise<TimeSeriesResult> {
  const online = await isServiceOnline(`${env.PROMETHEUS_URL}/api/v1/status/config`);
  if (!online) return {};

  const rangeSec = ALLOWED_RANGES[range];
  const stepSec = ALLOWED_STEPS[step];
  if (!rangeSec || !stepSec) return {};

  const now = Math.floor(Date.now() / 1000);
  const start = now - rangeSec;

  const result: TimeSeriesResult = {};

  await Promise.all(
    metricKeys
      .filter(k => METRIC_QUERIES[k])
      .map(async (key) => {
        try {
          const query = METRIC_QUERIES[key];
          const url = `${env.PROMETHEUS_URL}/api/v1/query_range?query=${encodeURIComponent(query)}&start=${start}&end=${now}&step=${stepSec}`;
          const response = await fetchWithTimeout(url, {}, 8000);
          const data = await response.json() as {
            data?: { result?: Array<{ values?: Array<[number, string]> }> };
          };
          const values = data?.data?.result?.[0]?.values || [];
          result[key] = {
            timestamps: values.map(v => v[0]),
            values: values.map(v => {
              const n = parseFloat(v[1]);
              return isNaN(n) || !isFinite(n) ? 0 : Math.round(n * 1000) / 1000;
            }),
          };
        } catch (err) {
          logger.debug(`Failed to fetch time-series for ${key}`, err);
          result[key] = { timestamps: [], values: [] };
        }
      }),
  );

  return result;
}

// --- Container Resources from cAdvisor Prometheus metrics ---

export interface ContainerResource {
  name: string;
  label: string;
  cpuPercent: number;
  memoryMB: number;
  memoryLimitMB: number;
  networkRxKBps: number;
  networkTxKBps: number;
}

export async function getContainerResources(): Promise<ContainerResource[]> {
  const online = await isServiceOnline(`${env.PROMETHEUS_URL}/api/v1/status/config`);
  if (!online) return [];

  // Auto-discover containers from Docker network
  const containers = await dockerService.listNetworkContainers();
  if (containers.length === 0) return [];

  const containerNames = containers.map(c => c.name).join('|');
  const nameFilter = `name=~"${containerNames}"`;

  const queries = [
    `rate(container_cpu_usage_seconds_total{${nameFilter}}[1m])`,
    `container_memory_usage_bytes{${nameFilter}}`,
    `container_spec_memory_limit_bytes{${nameFilter}}`,
    `rate(container_network_receive_bytes_total{${nameFilter}}[1m])`,
    `rate(container_network_transmit_bytes_total{${nameFilter}}[1m])`,
  ];

  try {
    const results = await Promise.all(
      queries.map(async (q) => {
        const response = await fetchWithTimeout(
          `${env.PROMETHEUS_URL}/api/v1/query?query=${encodeURIComponent(q)}`,
          {},
          5000,
        );
        const data = await response.json() as {
          data?: { result?: Array<{ metric?: { name?: string }; value?: [number, string] }> };
        };
        return data?.data?.result || [];
      }),
    );

    const [cpuResults, memResults, memLimitResults, rxResults, txResults] = results;

    const getVal = (results: typeof cpuResults, containerName: string): number => {
      const item = results.find(r => r.metric?.name === containerName);
      const v = parseFloat(item?.value?.[1] || '0');
      return isNaN(v) || !isFinite(v) ? 0 : v;
    };

    return containers.map(c => ({
      name: c.name,
      label: c.label,
      cpuPercent: Math.round(getVal(cpuResults, c.name) * 100 * 100) / 100,
      memoryMB: Math.round(getVal(memResults, c.name) / 1048576),
      memoryLimitMB: Math.round(getVal(memLimitResults, c.name) / 1048576),
      networkRxKBps: Math.round(getVal(rxResults, c.name) / 1024 * 100) / 100,
      networkTxKBps: Math.round(getVal(txResults, c.name) / 1024 * 100) / 100,
    }));
  } catch (err) {
    logger.warn('Failed to fetch container resources', err);
    return [];
  }
}

// --- API Metrics from Prometheus ---

export interface ApiMetrics {
  requestRate: number;       // req/s over last 5m
  errorRate: number;         // 4xx+5xx rate
  avgLatencyMs: number;      // average response time
  p95LatencyMs: number;      // 95th percentile
  topRoutes: { method: string; route: string; count: number }[];
  slowRoutes: { method: string; route: string; p95Ms: number }[];
  statusBreakdown: { status: string; count: number }[];
}

async function queryPrometheus(query: string): Promise<any> {
  const response = await fetchWithTimeout(
    `${env.PROMETHEUS_URL}/api/v1/query?query=${encodeURIComponent(query)}`,
    {},
    5000,
  );
  return response.json();
}

export async function getApiMetrics(): Promise<ApiMetrics | null> {
  try {
    const online = await isServiceOnline(`${env.PROMETHEUS_URL}/api/v1/status/config`);
    if (!online) return null;

    const queries = [
      // Total request rate
      'sum(rate(http_requests_total[5m]))',
      // Error rate (4xx + 5xx)
      'sum(rate(http_requests_total{status_code=~"[45].."}[5m]))',
      // Average latency
      'sum(rate(http_request_duration_seconds_sum[5m])) / sum(rate(http_request_duration_seconds_count[5m]))',
      // P95 latency
      'histogram_quantile(0.95, sum by(le) (rate(http_request_duration_seconds_bucket[5m])))',
      // Top 10 routes by request count (last 15m for more data)
      'topk(10, sum by(method, route) (increase(http_requests_total[15m])))',
      // Slowest 10 routes by p95
      'topk(10, histogram_quantile(0.95, sum by(method, route, le) (rate(http_request_duration_seconds_bucket[5m]))))',
      // Status code breakdown
      'sum by(status_code) (increase(http_requests_total[15m]))',
    ];

    validatePromQLQueries(queries);

    const [rateRes, errorRes, avgLatRes, p95Res, topRes, slowRes, statusRes] = await Promise.all(
      queries.map(q => queryPrometheus(q).catch(() => null)),
    );

    const extractScalar = (res: any): number => {
      const val = parseFloat(res?.data?.result?.[0]?.value?.[1] || '0');
      return isNaN(val) || !isFinite(val) ? 0 : val;
    };

    // Parse top routes
    const topRoutes: ApiMetrics['topRoutes'] = [];
    for (const item of (topRes?.data?.result || [])) {
      const route = item.metric?.route || '';
      // Skip internal routes
      if (route === '/api/metrics' || route === '/api/health') continue;
      const count = parseFloat(item.value?.[1] || '0');
      if (count > 0) {
        topRoutes.push({
          method: item.metric?.method || '?',
          route,
          count: Math.round(count),
        });
      }
    }

    // Parse slow routes
    const slowRoutes: ApiMetrics['slowRoutes'] = [];
    for (const item of (slowRes?.data?.result || [])) {
      const route = item.metric?.route || '';
      if (route === '/api/metrics' || route === '/api/health') continue;
      const p95 = parseFloat(item.value?.[1] || '0');
      if (p95 > 0 && isFinite(p95)) {
        slowRoutes.push({
          method: item.metric?.method || '?',
          route,
          p95Ms: Math.round(p95 * 1000),
        });
      }
    }
    slowRoutes.sort((a, b) => b.p95Ms - a.p95Ms);

    // Parse status breakdown
    const statusBreakdown: ApiMetrics['statusBreakdown'] = [];
    for (const item of (statusRes?.data?.result || [])) {
      const count = parseFloat(item.value?.[1] || '0');
      if (count > 0) {
        statusBreakdown.push({
          status: item.metric?.status_code || '?',
          count: Math.round(count),
        });
      }
    }
    statusBreakdown.sort((a, b) => b.count - a.count);

    return {
      requestRate: Math.round(extractScalar(rateRes) * 100) / 100,
      errorRate: Math.round(extractScalar(errorRes) * 100) / 100,
      avgLatencyMs: Math.round(extractScalar(avgLatRes) * 1000),
      p95LatencyMs: Math.round(extractScalar(p95Res) * 1000),
      topRoutes: topRoutes.slice(0, 8),
      slowRoutes: slowRoutes.slice(0, 5),
      statusBreakdown,
    };
  } catch (err) {
    logger.warn('Failed to fetch API metrics from Prometheus', err);
    return null;
  }
}

// --- Today's Events from Gancio ---

export interface TodayEvent {
  id: number;
  title: string;
  description: string;
  placeName: string;
  startTime: string;   // ISO string
  endTime: string | null;
  tags: string[];
}

export interface TodayEventsResult {
  enabled: boolean;
  events: TodayEvent[];
  total: number;
}

export async function getTodayEvents(): Promise<TodayEventsResult> {
  if (!gancioClient.enabled) {
    return { enabled: false, events: [], total: 0 };
  }

  try {
    // Gancio public API: GET /api/events returns all upcoming events
    const url = `${env.GANCIO_URL}/api/events`;
    const res = await fetch(url, { signal: AbortSignal.timeout(5000) });

    if (!res.ok) {
      logger.debug(`Gancio events API returned ${res.status}`);
      return { enabled: true, events: [], total: 0 };
    }

    const rawEvents = await res.json() as Array<{
      id: number;
      title: string;
      description: string;
      place_name: string;
      place_address: string;
      start_datetime: number;
      end_datetime?: number;
      tags: string[];
    }>;

    // Filter to today's events (UTC-based day boundaries matching local server time)
    const now = new Date();
    const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const todayEnd = new Date(todayStart.getTime() + 86400000);
    const todayStartUnix = Math.floor(todayStart.getTime() / 1000);
    const todayEndUnix = Math.floor(todayEnd.getTime() / 1000);

    const todayEvents = rawEvents
      .filter(e => e.start_datetime >= todayStartUnix && e.start_datetime < todayEndUnix)
      .sort((a, b) => a.start_datetime - b.start_datetime)
      .slice(0, 20)
      .map(e => ({
        id: e.id,
        title: e.title,
        description: (e.description || '').slice(0, 200),
        placeName: e.place_name || '',
        startTime: new Date(e.start_datetime * 1000).toISOString(),
        endTime: e.end_datetime ? new Date(e.end_datetime * 1000).toISOString() : null,
        tags: Array.isArray(e.tags) ? e.tags : [],
      }));

    return { enabled: true, events: todayEvents, total: todayEvents.length };
  } catch (err) {
    logger.debug('Failed to fetch today events from Gancio:', err);
    return { enabled: true, events: [], total: 0 };
  }
}

// --- Upcoming Shifts ---

export interface UpcomingShiftItem {
  id: string;
  title: string;
  date: string;
  startTime: string;
  endTime: string;
  location: string | null;
  maxVolunteers: number;
  currentVolunteers: number;
  status: string;
  cutName: string | null;
}

export interface UpcomingShiftsResult {
  shifts: UpcomingShiftItem[];
  total: number;
}

export async function getUpcomingShifts(): Promise<UpcomingShiftsResult> {
  try {
    const now = new Date();
    const [shifts, total] = await Promise.all([
      prisma.shift.findMany({
        where: { date: { gte: now }, status: { not: 'CANCELLED' } },
        orderBy: { date: 'asc' },
        take: 5,
        select: {
          id: true,
          title: true,
          date: true,
          startTime: true,
          endTime: true,
          location: true,
          maxVolunteers: true,
          currentVolunteers: true,
          status: true,
          cut: { select: { name: true } },
        },
      }),
      prisma.shift.count({ where: { date: { gte: now }, status: { not: 'CANCELLED' } } }),
    ]);

    return {
      shifts: shifts.map(s => ({
        id: s.id,
        title: s.title,
        date: s.date.toISOString(),
        startTime: s.startTime,
        endTime: s.endTime,
        location: s.location,
        maxVolunteers: s.maxVolunteers,
        currentVolunteers: s.currentVolunteers,
        status: s.status,
        cutName: s.cut?.name || null,
      })),
      total,
    };
  } catch (err) {
    logger.debug('Failed to fetch upcoming shifts', err);
    return { shifts: [], total: 0 };
  }
}

// --- Recent Shift Signups ---

export interface RecentSignupItem {
  id: string;
  userName: string | null;
  userEmail: string;
  shiftTitle: string | null;
  shiftDate: string | null;
  signupDate: string;
  signupSource: string;
}

export interface RecentSignupsResult {
  signups: RecentSignupItem[];
  total: number;
}

export async function getRecentSignups(): Promise<RecentSignupsResult> {
  try {
    const since = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000); // last 14 days
    const [signups, total] = await Promise.all([
      prisma.shiftSignup.findMany({
        where: { signupDate: { gte: since }, status: 'CONFIRMED' },
        orderBy: { signupDate: 'desc' },
        take: 8,
        select: {
          id: true,
          userName: true,
          userEmail: true,
          shiftTitle: true,
          signupDate: true,
          signupSource: true,
          shift: { select: { date: true } },
        },
      }),
      prisma.shiftSignup.count({ where: { signupDate: { gte: since }, status: 'CONFIRMED' } }),
    ]);

    return {
      signups: signups.map(s => ({
        id: s.id,
        userName: s.userName,
        userEmail: s.userEmail,
        shiftTitle: s.shiftTitle,
        shiftDate: s.shift.date.toISOString(),
        signupDate: s.signupDate.toISOString(),
        signupSource: s.signupSource,
      })),
      total,
    };
  } catch (err) {
    logger.debug('Failed to fetch recent signups', err);
    return { signups: [], total: 0 };
  }
}

// --- Top Videos (Media) ---

export interface TopVideoItem {
  id: number;
  title: string | null;
  filename: string;
  viewCount: number;
  commentCount: number;
  upvoteCount: number;
  durationSeconds: number | null;
  isPublished: boolean;
}

export interface TopVideosResult {
  enabled: boolean;
  videos: TopVideoItem[];
}

export async function getTopVideos(): Promise<TopVideosResult> {
  if (env.ENABLE_MEDIA_FEATURES !== 'true') {
    return { enabled: false, videos: [] };
  }

  try {
    const videos = await prisma.video.findMany({
      select: {
        id: true,
        title: true,
        filename: true,
        viewCount: true,
        commentCount: true,
        upvoteCount: true,
        durationSeconds: true,
        isPublished: true,
      },
      orderBy: { viewCount: 'desc' },
      take: 5,
    });

    return {
      enabled: true,
      videos: videos.map(v => ({
        id: v.id,
        title: v.title,
        filename: v.filename,
        viewCount: v.viewCount,
        commentCount: v.commentCount,
        upvoteCount: v.upvoteCount,
        durationSeconds: v.durationSeconds,
        isPublished: v.isPublished,
      })),
    };
  } catch (err) {
    logger.debug('Failed to fetch top videos', err);
    return { enabled: true, videos: [] };
  }
}

// --- Recent Comments (Media) ---

export interface RecentCommentItem {
  id: number;
  content: string;
  videoId: number;
  videoTitle: string | null;
  videoFilename: string;
  authorName: string | null;
  safetyStatus: string | null;
  createdAt: string;
}

export interface RecentCommentsResult {
  enabled: boolean;
  comments: RecentCommentItem[];
  pendingCount: number;
}

export async function getRecentComments(): Promise<RecentCommentsResult> {
  if (env.ENABLE_MEDIA_FEATURES !== 'true') {
    return { enabled: false, comments: [], pendingCount: 0 };
  }

  try {
    const [comments, pendingCount] = await Promise.all([
      prisma.comment.findMany({
        where: { isHidden: { not: true } },
        include: {
          user: { select: { name: true, email: true } },
          media: { select: { id: true, title: true, filename: true } },
        },
        orderBy: { createdAt: 'desc' },
        take: 8,
      }),
      prisma.comment.count({ where: { safetyStatus: 'pending' } }),
    ]);

    return {
      enabled: true,
      comments: comments.map(c => ({
        id: c.id,
        content: c.content.slice(0, 200),
        videoId: c.media.id,
        videoTitle: c.media.title,
        videoFilename: c.media.filename,
        authorName: c.user?.name || c.user?.email || null,
        safetyStatus: c.safetyStatus,
        createdAt: c.createdAt.toISOString(),
      })),
      pendingCount,
    };
  } catch (err) {
    logger.debug('Failed to fetch recent comments', err);
    return { enabled: true, comments: [], pendingCount: 0 };
  }
}

// --- Chat Summary from Rocket.Chat ---

export interface ChatMessage {
  id: string;
  channel: string;
  username: string;
  text: string;
  timestamp: string;
  isBot: boolean;
}

export interface ChatSummaryResult {
  enabled: boolean;
  messages: ChatMessage[];
  unreadChannels: number;
}

export async function getChatSummary(): Promise<ChatSummaryResult> {
  const chatEnabled = env.ENABLE_CHAT === 'true';
  if (!chatEnabled) {
    return { enabled: false, messages: [], unreadChannels: 0 };
  }

  try {
    const online = await rocketchatClient.healthCheck();
    if (!online) {
      return { enabled: true, messages: [], unreadChannels: 0 };
    }

    // Fetch recent messages from our notification channels
    const channels = ['general', 'shifts', 'canvassing', 'campaigns'];
    const allMessages: ChatMessage[] = [];

    await Promise.all(
      channels.map(async (channelName) => {
        const messages = await rocketchatClient.getChannelHistory(channelName, 5);
        for (const msg of messages) {
          if (!msg.msg?.trim()) continue;
          allMessages.push({
            id: msg._id,
            channel: channelName,
            username: msg.alias || msg.u?.username || 'unknown',
            text: msg.msg.slice(0, 300),
            timestamp: msg.ts,
            isBot: !!(msg.bot || msg.alias),
          });
        }
      }),
    );

    // Sort by timestamp descending, take most recent 15
    allMessages.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
    const messages = allMessages.slice(0, 15);

    return {
      enabled: true,
      messages,
      unreadChannels: 0,
    };
  } catch (err) {
    logger.debug('Failed to fetch chat summary:', err);
    return { enabled: true, messages: [], unreadChannels: 0 };
  }
}

// --- Redis-Cached Service Dashboard Endpoints ---

/**
 * Helper: get JSON from Redis cache or fetch from source, then cache with TTL.
 */
async function cachedFetch<T>(
  key: string,
  ttlSeconds: number,
  fetcher: () => Promise<T>,
): Promise<T> {
  try {
    const cached = await redis.get(key);
    if (cached) return JSON.parse(cached) as T;
  } catch {
    // Redis unavailable, fall through to live fetch
  }

  const data = await fetcher();

  try {
    await redis.set(key, JSON.stringify(data), 'EX', ttlSeconds);
  } catch {
    // Redis unavailable, continue without caching
  }

  return data;
}

// --- Gitea Activity ---

export interface GiteaActivityResult {
  repos: number;
  users: number;
  recentCommits: Array<{
    repo: string;
    message: string;
    author: string;
    date: string;
  }>;
}

export async function getGiteaActivity(): Promise<GiteaActivityResult | { error: string }> {
  try {
    const available = await giteaClient.isAvailable();
    if (!available) return { error: 'Service unavailable' };

    return cachedFetch<GiteaActivityResult>('dashboard:gitea-activity', 300, async () => {
      const [repos, users] = await Promise.all([
        giteaClient.listRepos(50),
        giteaClient.listAllUsers(100),
      ]);

      // Fetch recent commits from the 5 most recently updated repos
      const recentRepos = (Array.isArray(repos) ? repos : (repos as any).data || []).slice(0, 5);
      const commitResults = await Promise.all(
        recentRepos.map(async (r: any) => {
          const parts = (r.full_name || '').split('/');
          if (parts.length < 2) return [];
          const commits = await giteaClient.listRepoCommits(parts[0], parts[1], 3);
          return commits.map(c => ({
            repo: r.name || r.full_name,
            message: (c.commit?.message || '').split('\n')[0].slice(0, 80),
            author: c.commit?.author?.name || 'unknown',
            date: c.commit?.author?.date || '',
          }));
        }),
      );

      const recentCommits = commitResults
        .flat()
        .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
        .slice(0, 10);

      const repoList = Array.isArray(repos) ? repos : (repos as any).data || [];

      return {
        repos: repoList.length,
        users: users.length,
        recentCommits,
      };
    });
  } catch (err) {
    logger.debug('Failed to fetch Gitea activity:', err);
    return { error: 'Service unavailable' };
  }
}

// --- Rocket.Chat Stats ---

export interface RocketChatStatsResult {
  totalUsers: number;
  onlineUsers: number;
  channels: number;
  totalMessages: number;
  topChannels: Array<{ name: string; messages: number; users: number }>;
}

export async function getRocketChatStats(): Promise<RocketChatStatsResult | { error: string }> {
  try {
    const online = await rocketchatClient.healthCheck();
    if (!online) return { error: 'Service unavailable' };

    return cachedFetch<RocketChatStatsResult>('dashboard:rocketchat-stats', 180, async () => {
      const [stats, channels] = await Promise.all([
        rocketchatClient.getStatistics(),
        rocketchatClient.listChannels(10),
      ]);

      return {
        totalUsers: stats?.totalUsers || 0,
        onlineUsers: stats?.onlineUsers || 0,
        channels: stats?.totalChannels || 0,
        totalMessages: stats?.totalMessages || 0,
        topChannels: channels.map(c => ({
          name: c.name,
          messages: c.msgs,
          users: c.usersCount,
        })),
      };
    });
  } catch (err) {
    logger.debug('Failed to fetch Rocket.Chat stats:', err);
    return { error: 'Service unavailable' };
  }
}

// --- Vaultwarden Adoption ---

export interface VaultwardenAdoptionResult {
  total: number;
  enabled: number;
  invited: number;
  disabled: number;
  adoptionRate: number;
}

export async function getVaultwardenAdoption(): Promise<VaultwardenAdoptionResult | { error: string }> {
  try {
    if (!vaultwardenClient.hasCredentials) return { error: 'Service unavailable' };

    return cachedFetch<VaultwardenAdoptionResult>('dashboard:vaultwarden-adoption', 600, async () => {
      const users = await vaultwardenClient.listUsers();
      const enabled = users.filter(u => u._Status === 0).length;
      const invited = users.filter(u => u._Status === 1).length;
      const disabled = users.filter(u => u._Status === 2).length;
      const total = users.length;
      const adoptionRate = total > 0 ? Math.round((enabled / total) * 100) : 0;

      return { total, enabled, invited, disabled, adoptionRate };
    });
  } catch (err) {
    logger.debug('Failed to fetch Vaultwarden adoption:', err);
    return { error: 'Service unavailable' };
  }
}

// --- Listmonk Campaigns ---

export interface ListmonkCampaignItem {
  name: string;
  status: string;
  sentCount: number;
  openRate: number;
  clickRate: number;
}

export interface ListmonkCampaignsResult {
  campaigns: ListmonkCampaignItem[];
}

export async function getListmonkCampaigns(): Promise<ListmonkCampaignsResult | { error: string }> {
  try {
    const healthy = await listmonkClient.checkHealth();
    if (!healthy) return { error: 'Service unavailable' };

    return cachedFetch<ListmonkCampaignsResult>('dashboard:listmonk-campaigns', 300, async () => {
      const rawCampaigns = await listmonkClient.getCampaigns();

      const campaigns: ListmonkCampaignItem[] = rawCampaigns.slice(0, 15).map(c => {
        const openRate = c.sent > 0 ? Math.round((c.views / c.sent) * 100) : 0;
        const clickRate = c.sent > 0 ? Math.round((c.clicks / c.sent) * 100) : 0;
        return {
          name: c.name,
          status: c.status,
          sentCount: c.sent,
          openRate,
          clickRate,
        };
      });

      return { campaigns };
    });
  } catch (err) {
    logger.debug('Failed to fetch Listmonk campaigns:', err);
    return { error: 'Service unavailable' };
  }
}
