import { useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import {
  Activity,
  AlertTriangle,
  CheckCircle2,
  ChevronDown,
  Clock3,
  Layers3,
  LayoutDashboard,
  ShieldCheck,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom";
import {
  OverviewAxisNotes,
  OverviewMetric,
  OverviewSelectionChips,
} from "../../../../common/core/components/overview";
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
  type ClientSummary,
  fetchClients,
  fetchDevAuditLogs,
  fetchDevRPUsageDaily,
  fetchDevStats,
  fetchDevUser,
  type RPUsageDailyMetric,
  type RPUsagePeriod,
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { resolveProfileRole } from "../../lib/role";
import { fetchMe } from "../auth/authApi";
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
import {
  buildRecentClientChanges,
  type RecentClientChange,
} from "./recentClientChanges";

type ClientDistribution = {
  activeClients: number;
  headlessClients: number;
  pkceClients: number;
  privateClients: number;
};

type DailyPoint = {
  date: string;
  loginRequests: number;
  otherRequests: number;
};

type SeriesSummary = {
  key: string;
  clientLabel: string;
  loginRequests: number;
  otherRequests: number;
  uniqueSubjects: number;
};

type MultiLineSeries = {
  key: string;
  clientLabel: string;
  color: UsageChartPalette;
  points: DailyPoint[];
};

type ClientFilterOption = {
  id: string;
  label: string;
};

type UsageChartPalette = {
  bar: string;
  line: string;
  point: string;
};

const deletedRecentChangeFilterId = "__deleted_recent_clients__";
const localeStorageKey = "locale";

type RecentChangePoint = {
  date: string;
  changeCount: number;
  uniqueActors: number;
};

type RecentChangeSeriesSummary = {
  key: string;
  clientLabel: string;
  changeCount: number;
  uniqueActors: number;
};

type RecentChangeSeries = {
  key: string;
  clientLabel: string;
  color: UsageChartPalette;
  points: RecentChangePoint[];
};

type AppLocale = "ko" | "en";

function resolveAppLocale(): AppLocale {
  if (typeof window === "undefined") {
    return "ko";
  }

  const stored = window.localStorage.getItem(localeStorageKey);
  if (stored === "ko" || stored === "en") {
    return stored;
  }

  const pathLocale = window.location.pathname.split("/")[1];
  if (pathLocale === "ko" || pathLocale === "en") {
    return pathLocale;
  }

  return window.navigator.language.toLowerCase().startsWith("ko") ? "ko" : "en";
}

function formatRecentChangeTimestamp(value: string) {
  if (!value) {
    return { date: "-", time: "-" };
  }

  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) {
    return { date: value, time: "-" };
  }

  const locale = resolveAppLocale();
  if (locale === "ko") {
    const date = parsed.toISOString().slice(0, 10);
    const timeParts = new Intl.DateTimeFormat("ko-KR", {
      hour12: false,
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
    }).formatToParts(parsed);
    const hour = timeParts.find((part) => part.type === "hour")?.value ?? "00";
    const minute =
      timeParts.find((part) => part.type === "minute")?.value ?? "00";
    const second =
      timeParts.find((part) => part.type === "second")?.value ?? "00";

    return {
      date,
      time: `${hour}시 ${minute}분 ${second}초`,
    };
  }

  return {
    date: new Intl.DateTimeFormat("en-US", {
      year: "numeric",
      month: "short",
      day: "numeric",
    }).format(parsed),
    time: new Intl.DateTimeFormat("en-US", {
      hour: "numeric",
      minute: "2-digit",
      second: "2-digit",
    }).format(parsed),
  };
}

const usageChartPalettes: UsageChartPalette[] = [
  { bar: "#7dd3fc", line: "#10b981", point: "#059669" },
  { bar: "#f9a8d4", line: "#f97316", point: "#ea580c" },
  { bar: "#c4b5fd", line: "#6366f1", point: "#4f46e5" },
  { bar: "#86efac", line: "#14b8a6", point: "#0f766e" },
  { bar: "#fdba74", line: "#ef4444", point: "#dc2626" },
  { bar: "#93c5fd", line: "#8b5cf6", point: "#7c3aed" },
];

function buildClientDistribution(clients: ClientSummary[]): ClientDistribution {
  return clients.reduce<ClientDistribution>(
    (summary, client) => {
      if (client.status === "active") {
        summary.activeClients += 1;
      }

      if (client.metadata?.headless_login_enabled === true) {
        summary.headlessClients += 1;
      }

      if (client.type === "pkce") {
        summary.pkceClients += 1;
      } else {
        summary.privateClients += 1;
      }

      return summary;
    },
    {
      activeClients: 0,
      headlessClients: 0,
      pkceClients: 0,
      privateClients: 0,
    },
  );
}

function summarizeDaily(rows: RPUsageDailyMetric[]): DailyPoint[] {
  const byDate = new Map<string, DailyPoint>();
  for (const row of rows) {
    const current =
      byDate.get(row.date) ??
      ({
        date: row.date,
        loginRequests: 0,
        otherRequests: 0,
      } satisfies DailyPoint);
    current.loginRequests += row.loginRequests;
    current.otherRequests += row.otherRequests;
    byDate.set(row.date, current);
  }
  return Array.from(byDate.values()).sort((left, right) =>
    left.date.localeCompare(right.date),
  );
}

function summarizeSeries(rows: RPUsageDailyMetric[]): SeriesSummary[] {
  const bySeries = new Map<string, SeriesSummary>();
  for (const row of rows) {
    const key = row.clientId;
    const current =
      bySeries.get(key) ??
      ({
        key,
        clientLabel: row.clientName || row.clientId,
        loginRequests: 0,
        otherRequests: 0,
        uniqueSubjects: 0,
      } satisfies SeriesSummary);
    current.loginRequests += row.loginRequests;
    current.otherRequests += row.otherRequests;
    current.uniqueSubjects = Math.max(
      current.uniqueSubjects,
      row.uniqueSubjects,
    );
    bySeries.set(key, current);
  }
  return Array.from(bySeries.values()).sort(
    (left, right) =>
      right.loginRequests +
      right.otherRequests -
      (left.loginRequests + left.otherRequests),
  );
}

function toPeriodBucket(date: string, period: RPUsagePeriod) {
  const parts = parseDateParts(date);
  if (!parts) {
    return date;
  }
  if (period === "month") {
    return `${parts.year}-${parts.monthText}-01`;
  }
  if (period === "week") {
    const weekThursday = getISOWeekThursday(parts.year, parts.month, parts.day);
    const weekYear = weekThursday.getUTCFullYear();
    const weekMonth = String(weekThursday.getUTCMonth() + 1).padStart(2, "0");
    const weekDay = String(weekThursday.getUTCDate()).padStart(2, "0");
    return `${weekYear}-${weekMonth}-${weekDay}`;
  }
  return date;
}

function buildMultiLineSeries(rows: RPUsageDailyMetric[]): MultiLineSeries[] {
  const dates = summarizeDaily(rows).map((point) => point.date);
  const byClient = new Map<
    string,
    {
      clientLabel: string;
      byDate: Map<string, DailyPoint>;
    }
  >();

  for (const row of rows) {
    const current = byClient.get(row.clientId) ?? {
      clientLabel: row.clientName || row.clientId,
      byDate: new Map<string, DailyPoint>(),
    };
    const point =
      current.byDate.get(row.date) ??
      ({
        date: row.date,
        loginRequests: 0,
        otherRequests: 0,
      } satisfies DailyPoint);
    point.loginRequests += row.loginRequests;
    point.otherRequests += row.otherRequests;
    current.byDate.set(row.date, point);
    byClient.set(row.clientId, current);
  }

  return Array.from(byClient.entries())
    .sort((left, right) =>
      left[1].clientLabel.localeCompare(right[1].clientLabel),
    )
    .map(([clientId, entry], index) => ({
      key: clientId,
      clientLabel: entry.clientLabel,
      color: usageChartPalettes[index % usageChartPalettes.length],
      points: dates.map(
        (date) =>
          entry.byDate.get(date) ??
          ({
            date,
            loginRequests: 0,
            otherRequests: 0,
          } satisfies DailyPoint),
      ),
    }));
}

function parseDateParts(date: string) {
  const parts = date.split("-");
  if (parts.length === 3) {
    return {
      year: Number(parts[0]),
      month: Number(parts[1]),
      day: Number(parts[2]),
      monthText: parts[1],
      dayText: parts[2],
    };
  }
  return null;
}

function getISOWeekNumber(year: number, month: number, day: number) {
  const date = new Date(Date.UTC(year, month - 1, day));
  const dayOfWeek = date.getUTCDay() || 7;
  date.setUTCDate(date.getUTCDate() + 4 - dayOfWeek);
  const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
  return Math.ceil(((date.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
}

function getISOWeekThursday(year: number, month: number, day: number) {
  const date = new Date(Date.UTC(year, month - 1, day));
  const dayOfWeek = date.getUTCDay() || 7;
  date.setUTCDate(date.getUTCDate() + 4 - dayOfWeek);
  return date;
}

function formatDate(value?: string) {
  if (!value) {
    return "-";
  }
  const parsed = new Date(value);
  if (Number.isNaN(parsed.getTime())) {
    return value;
  }
  return parsed.toLocaleDateString();
}

function formatPeriodLabel(date: string, period: RPUsagePeriod) {
  const parts = parseDateParts(date);
  if (!parts) {
    return date;
  }
  if (period === "month") {
    return `${parts.monthText}월`;
  }
  if (period === "week") {
    const weekNumber = String(
      getISOWeekNumber(parts.year, parts.month, parts.day),
    ).padStart(2, "0");
    const weekThursday = getISOWeekThursday(parts.year, parts.month, parts.day);
    const weekMonth = weekThursday.getUTCMonth() + 1;
    const weekDay = weekThursday.getUTCDate();
    const weekMonthText = String(weekMonth).padStart(2, "0");
    const weekOfMonth = Math.min(5, Math.max(1, Math.ceil(weekDay / 7)));
    return `${weekNumber}(${weekMonthText}월${weekOfMonth}주)`;
  }
  return `${parts.monthText}.${parts.dayText}`;
}

function formatMetric(value: number | undefined) {
  return value === undefined ? "-" : value.toLocaleString();
}

function summarizeRecentChanges(
  items: RecentClientChange[],
  period: RPUsagePeriod,
): RecentChangePoint[] {
  const byDate = new Map<
    string,
    { changeCount: number; actors: Set<string> }
  >();
  for (const item of items) {
    const bucket = toPeriodBucket(item.timestamp.slice(0, 10), period);
    const current = byDate.get(bucket) ?? {
      changeCount: 0,
      actors: new Set<string>(),
    };
    current.changeCount += 1;
    if (item.actorId && item.actorId !== "-") {
      current.actors.add(item.actorId);
    }
    byDate.set(bucket, current);
  }

  return Array.from(byDate.entries())
    .map(([date, current]) => ({
      date,
      changeCount: current.changeCount,
      uniqueActors: current.actors.size,
    }))
    .sort((left, right) => left.date.localeCompare(right.date));
}

function summarizeRecentChangeSeries(
  items: RecentClientChange[],
): RecentChangeSeriesSummary[] {
  const bySeries = new Map<
    string,
    { clientLabel: string; changeCount: number; actors: Set<string> }
  >();
  for (const item of items) {
    const current = bySeries.get(item.clientId) ?? {
      clientLabel: item.clientName,
      changeCount: 0,
      actors: new Set<string>(),
    };
    current.changeCount += 1;
    if (item.actorId && item.actorId !== "-") {
      current.actors.add(item.actorId);
    }
    bySeries.set(item.clientId, current);
  }

  return Array.from(bySeries.entries())
    .map(([key, current]) => ({
      key,
      clientLabel: current.clientLabel,
      changeCount: current.changeCount,
      uniqueActors: current.actors.size,
    }))
    .sort((left, right) => right.changeCount - left.changeCount);
}

function buildRecentChangeSeries(
  items: RecentClientChange[],
  period: RPUsagePeriod,
): RecentChangeSeries[] {
  const dates = summarizeRecentChanges(items, period).map(
    (point) => point.date,
  );
  const byClient = new Map<
    string,
    {
      clientLabel: string;
      byDate: Map<string, RecentChangePoint>;
      actorIdsByDate: Map<string, Set<string>>;
    }
  >();

  for (const item of items) {
    const bucket = toPeriodBucket(item.timestamp.slice(0, 10), period);
    const current = byClient.get(item.clientId) ?? {
      clientLabel: item.clientName,
      byDate: new Map<string, RecentChangePoint>(),
      actorIdsByDate: new Map<string, Set<string>>(),
    };
    const point = current.byDate.get(bucket) ?? {
      date: bucket,
      changeCount: 0,
      uniqueActors: 0,
    };
    point.changeCount += 1;
    const actorIds = current.actorIdsByDate.get(bucket) ?? new Set<string>();
    if (item.actorId && item.actorId !== "-") {
      actorIds.add(item.actorId);
    }
    point.uniqueActors = actorIds.size;
    current.byDate.set(bucket, point);
    current.actorIdsByDate.set(bucket, actorIds);
    byClient.set(item.clientId, current);
  }

  return Array.from(byClient.entries())
    .sort((left, right) =>
      left[1].clientLabel.localeCompare(right[1].clientLabel),
    )
    .map(([clientId, entry], index) => ({
      key: clientId,
      clientLabel: entry.clientLabel,
      color: usageChartPalettes[index % usageChartPalettes.length],
      points: dates.map(
        (date) =>
          entry.byDate.get(date) ?? {
            date,
            changeCount: 0,
            uniqueActors: 0,
          },
      ),
    }));
}

function RPUsageMixedChart({
  period,
  rows,
  palette,
  multiLineSeries,
}: {
  period: RPUsagePeriod;
  rows: RPUsageDailyMetric[];
  palette?: UsageChartPalette;
  multiLineSeries?: MultiLineSeries[];
}) {
  const colors = palette ?? usageChartPalettes[0];
  const daily = summarizeDaily(rows);
  const series = summarizeSeries(rows);
  const topSeries = series.slice(0, 5);
  const seriesByKey = new Map(series.map((item) => [item.key, item]));
  const chartWidth = 720;
  const chartHeight = 230;
  const padX = 48;
  const padTop = 40;
  const padBottom = 34;
  const innerWidth = chartWidth - padX * 2;
  const innerHeight = chartHeight - padTop - padBottom;
  const maxValue = Math.max(1, ...daily.map((point) => point.loginRequests));
  const slot = daily.length > 0 ? innerWidth / daily.length : innerWidth;
  const y = (value: number) =>
    padTop + innerHeight - (value / maxValue) * innerHeight;
  const x = (index: number) => padX + slot * index + slot / 2;
  const linePoints = daily
    .map((point, index) => `${x(index)},${y(point.loginRequests)}`)
    .join(" ");
  const multiLinePoints = multiLineSeries?.map((seriesItem) => ({
    ...seriesItem,
    pointsAttr: seriesItem.points
      .map((point, index) => `${x(index)},${y(point.loginRequests)}`)
      .join(" "),
  }));

  if (daily.length === 0) {
    return (
      <div className="flex min-h-[210px] items-center justify-center text-sm text-muted-foreground">
        {t("msg.dev.dashboard.chart.empty", "표시할 RP 이용 집계가 없습니다.")}
      </div>
    );
  }

  return (
    <div className="space-y-3">
      <div className="overflow-x-auto">
        <svg
          role="img"
          aria-label={t("ui.dev.dashboard.chart.aria", "RP 요청 현황")}
          viewBox={`0 0 ${chartWidth} ${chartHeight}`}
          className="h-[235px] min-w-[720px] w-full"
        >
          <title>{t("ui.dev.dashboard.chart.aria", "RP 요청 현황")}</title>
          {[0, 0.25, 0.5, 0.75, 1].map((ratio) => {
            const gridY = padTop + innerHeight * ratio;
            const label = Math.round(maxValue * (1 - ratio));
            return (
              <g key={ratio}>
                <line
                  x1={padX}
                  x2={chartWidth - padX}
                  y1={gridY}
                  y2={gridY}
                  stroke="currentColor"
                  className="text-border"
                  strokeWidth="1"
                />
                <text
                  x={padX - 12}
                  y={gridY + 4}
                  textAnchor="end"
                  className="fill-muted-foreground text-[11px]"
                >
                  {label}
                </text>
              </g>
            );
          })}
          {daily.map((point, index) => {
            const center = x(index);
            return (
              <g key={point.date}>
                <text
                  x={center}
                  y={chartHeight - 12}
                  textAnchor="middle"
                  className="fill-muted-foreground text-[11px]"
                >
                  {formatPeriodLabel(point.date, period)}
                </text>
              </g>
            );
          })}
          {!multiLinePoints || multiLinePoints.length === 0 ? (
            <>
              <polyline
                points={linePoints}
                fill="none"
                stroke={colors.line}
                strokeWidth="3"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
              {daily.map((point, index) => (
                <circle
                  key={`${point.date}-login`}
                  cx={x(index)}
                  cy={y(point.loginRequests)}
                  r="4"
                  fill={colors.point}
                  stroke="hsl(var(--background))"
                  strokeWidth="2"
                />
              ))}
            </>
          ) : (
            multiLinePoints.map((seriesItem) => (
              <g key={seriesItem.key}>
                <polyline
                  points={seriesItem.pointsAttr}
                  fill="none"
                  stroke={seriesItem.color.line}
                  strokeWidth="3"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
                {seriesItem.points.map((point, index) => (
                  <circle
                    key={`${seriesItem.key}-${point.date}`}
                    cx={x(index)}
                    cy={y(point.loginRequests)}
                    r="3.5"
                    fill={seriesItem.color.point}
                    stroke="hsl(var(--background))"
                    strokeWidth="2"
                  />
                ))}
              </g>
            ))
          )}
        </svg>
      </div>

      <OverviewAxisNotes
        xAxisLabel={t("ui.common.chart.axis.x", "X축: 기간")}
        yAxisLabel={t("ui.common.chart.axis.y", "Y축: 로그인 요청 수")}
      />

      {multiLinePoints && multiLinePoints.length > 0 ? (
        <div className="grid gap-x-6 gap-y-2 border-t border-border/60 pt-2 text-xs md:grid-cols-2 xl:grid-cols-3">
          {multiLinePoints.map((item) => {
            const seriesItem = seriesByKey.get(item.key);
            return (
              <div
                key={item.key}
                className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1"
              >
                <span
                  className="h-2.5 w-2.5 rounded-full"
                  style={{ backgroundColor: item.color.line }}
                />
                <span className="font-medium">{item.clientLabel}</span>
                {seriesItem ? (
                  <span className="whitespace-nowrap tabular-nums text-muted-foreground">
                    {t(
                      "ui.dev.dashboard.chart.series",
                      "로그인 {{login}} / 사용자 {{subjects}}",
                      {
                        login: seriesItem.loginRequests.toLocaleString(),
                        subjects: seriesItem.uniqueSubjects.toLocaleString(),
                      },
                    )}
                  </span>
                ) : null}
              </div>
            );
          })}
        </div>
      ) : topSeries.length > 0 ? (
        <div className="grid gap-x-6 gap-y-2 border-t border-border/60 pt-2 text-xs md:grid-cols-2 xl:grid-cols-3">
          {topSeries.map((item) => (
            <div
              key={item.key}
              className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1"
            >
              <span className="font-medium">{item.clientLabel}</span>
              <span className="whitespace-nowrap tabular-nums text-muted-foreground">
                {t(
                  "ui.dev.dashboard.chart.series",
                  "로그인 {{login}} / 사용자 {{subjects}}",
                  {
                    login: item.loginRequests.toLocaleString(),
                    subjects: item.uniqueSubjects.toLocaleString(),
                  },
                )}
              </span>
            </div>
          ))}
        </div>
      ) : null}
    </div>
  );
}

function RecentClientChangesChart({
  items,
  period,
  multiLineSeries,
}: {
  items: RecentClientChange[];
  period: RPUsagePeriod;
  multiLineSeries?: RecentChangeSeries[];
}) {
  const daily = summarizeRecentChanges(items, period);
  const series = summarizeRecentChangeSeries(items);
  const topSeries = series.slice(0, 5);
  const seriesByKey = new Map(series.map((item) => [item.key, item]));
  const chartWidth = 720;
  const chartHeight = 230;
  const padX = 48;
  const padTop = 40;
  const padBottom = 34;
  const innerWidth = chartWidth - padX * 2;
  const innerHeight = chartHeight - padTop - padBottom;
  const maxValue = Math.max(1, ...daily.map((point) => point.changeCount));
  const slot = daily.length > 0 ? innerWidth / daily.length : innerWidth;
  const y = (value: number) =>
    padTop + innerHeight - (value / maxValue) * innerHeight;
  const x = (index: number) => padX + slot * index + slot / 2;
  const linePoints = daily
    .map((point, index) => `${x(index)},${y(point.changeCount)}`)
    .join(" ");
  const multiLinePoints = multiLineSeries?.map((seriesItem) => ({
    ...seriesItem,
    pointsAttr: seriesItem.points
      .map((point, index) => `${x(index)},${y(point.changeCount)}`)
      .join(" "),
  }));

  if (daily.length === 0) {
    return (
      <div className="flex min-h-[210px] items-center justify-center text-sm text-muted-foreground">
        {t(
          "msg.dev.dashboard.recent_changes.empty",
          "최근 변경 로그가 아직 없습니다.",
        )}
      </div>
    );
  }

  return (
    <div className="space-y-3">
      <div className="overflow-x-auto">
        <svg
          role="img"
          aria-label={t(
            "ui.dev.dashboard.recent_changes.aria",
            "최근 변경된 앱 현황",
          )}
          viewBox={`0 0 ${chartWidth} ${chartHeight}`}
          className="h-[235px] min-w-[720px] w-full"
        >
          <title>
            {t("ui.dev.dashboard.recent_changes.aria", "최근 변경된 앱 현황")}
          </title>
          {[0, 0.25, 0.5, 0.75, 1].map((ratio) => {
            const gridY = padTop + innerHeight * ratio;
            const label = Math.round(maxValue * (1 - ratio));
            return (
              <g key={ratio}>
                <line
                  x1={padX}
                  x2={chartWidth - padX}
                  y1={gridY}
                  y2={gridY}
                  stroke="currentColor"
                  className="text-border"
                  strokeWidth="1"
                />
                <text
                  x={padX - 12}
                  y={gridY + 4}
                  textAnchor="end"
                  className="fill-muted-foreground text-[11px]"
                >
                  {label}
                </text>
              </g>
            );
          })}
          {daily.map((point, index) => (
            <g key={point.date}>
              <text
                x={x(index)}
                y={chartHeight - 12}
                textAnchor="middle"
                className="fill-muted-foreground text-[11px]"
              >
                {formatPeriodLabel(point.date, period)}
              </text>
            </g>
          ))}
          {!multiLinePoints || multiLinePoints.length === 0 ? (
            <>
              <polyline
                points={linePoints}
                fill="none"
                stroke={usageChartPalettes[1].line}
                strokeWidth="3"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
              {daily.map((point, index) => (
                <circle
                  key={`${point.date}-change`}
                  cx={x(index)}
                  cy={y(point.changeCount)}
                  r="4"
                  fill={usageChartPalettes[1].point}
                  stroke="hsl(var(--background))"
                  strokeWidth="2"
                />
              ))}
            </>
          ) : (
            multiLinePoints.map((seriesItem) => (
              <g key={seriesItem.key}>
                <polyline
                  points={seriesItem.pointsAttr}
                  fill="none"
                  stroke={seriesItem.color.line}
                  strokeWidth="3"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
                {seriesItem.points.map((point, index) => (
                  <circle
                    key={`${seriesItem.key}-${point.date}`}
                    cx={x(index)}
                    cy={y(point.changeCount)}
                    r="3.5"
                    fill={seriesItem.color.point}
                    stroke="hsl(var(--background))"
                    strokeWidth="2"
                  />
                ))}
              </g>
            ))
          )}
        </svg>
      </div>

      <OverviewAxisNotes
        xAxisLabel={t("ui.common.chart.axis.x", "X축: 기간")}
        yAxisLabel={t("ui.dev.dashboard.recent_changes.y_axis", "Y축: 변경 수")}
      />

      {multiLinePoints && multiLinePoints.length > 0 ? (
        <div className="grid gap-x-6 gap-y-2 border-t border-border/60 pt-2 text-xs md:grid-cols-2 xl:grid-cols-3">
          {multiLinePoints.map((item) => {
            const seriesItem = seriesByKey.get(item.key);
            return (
              <div
                key={item.key}
                className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1"
              >
                <span
                  className="h-2.5 w-2.5 rounded-full"
                  style={{ backgroundColor: item.color.line }}
                />
                <span className="font-medium">{item.clientLabel}</span>
                {seriesItem ? (
                  <span className="whitespace-nowrap tabular-nums text-muted-foreground">
                    {t(
                      "ui.dev.dashboard.recent_changes.series",
                      "변경 {{changes}} / 작업자 {{actors}}",
                      {
                        changes: seriesItem.changeCount.toLocaleString(),
                        actors: seriesItem.uniqueActors.toLocaleString(),
                      },
                    )}
                  </span>
                ) : null}
              </div>
            );
          })}
        </div>
      ) : topSeries.length > 0 ? (
        <div className="grid gap-x-6 gap-y-2 border-t border-border/60 pt-2 text-xs md:grid-cols-2 xl:grid-cols-3">
          {topSeries.map((item) => (
            <div
              key={item.key}
              className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1"
            >
              <span className="font-medium">{item.clientLabel}</span>
              <span className="whitespace-nowrap tabular-nums text-muted-foreground">
                {t(
                  "ui.dev.dashboard.recent_changes.series",
                  "변경 {{changes}} / 작업자 {{actors}}",
                  {
                    changes: item.changeCount.toLocaleString(),
                    actors: item.uniqueActors.toLocaleString(),
                  },
                )}
              </span>
            </div>
          ))}
        </div>
      ) : null}
    </div>
  );
}

function GlobalOverviewPage() {
  const navigate = useNavigate();
  const auth = useAuth();
  const hasAccessToken = Boolean(auth.user?.access_token);
  const userProfile = auth.user?.profile as Record<string, unknown> | undefined;
  const role = resolveProfileRole(userProfile);
  const tenantId = userProfile?.tenant_id as string | undefined;
  const { data: me, isLoading: isLoadingMe } = useQuery({
    queryKey: ["userMe"],
    queryFn: fetchMe,
    enabled: hasAccessToken,
  });
  const profileRole = me?.role?.trim() || role;
  const [period, setPeriod] = useState<RPUsagePeriod>("day");
  const [selectedClientIds, setSelectedClientIds] = useState<string[]>([]);
  const [recentChangesPeriod, setRecentChangesPeriod] =
    useState<RPUsagePeriod>("week");
  const [selectedRecentChangeClientIds, setSelectedRecentChangeClientIds] =
    useState<string[]>([]);
  const [visibleRecentClientChangesCount, setVisibleRecentClientChangesCount] =
    useState(6);
  const [isRecentChangesDetailOpen, setIsRecentChangesDetailOpen] =
    useState(true);
  const usageDays = period === "day" ? 14 : period === "week" ? 84 : 90;
  const statsQuery = useQuery({
    queryKey: ["dev-dashboard-stats"],
    queryFn: fetchDevStats,
    retry: false,
  });
  const clientsQuery = useQuery({
    queryKey: ["dev-dashboard-clients"],
    queryFn: fetchClients,
    retry: false,
  });
  const usageQuery = useQuery({
    queryKey: ["dev-dashboard-rp-usage", usageDays, period],
    queryFn: () =>
      fetchDevRPUsageDaily({
        days: usageDays,
        period,
      }),
    retry: false,
  });

  const clients = clientsQuery.data?.items ?? [];
  const {
    hasDeveloperAccess,
    isDeveloperRequestPending,
    canRequestDeveloperAccess,
    isLoadingDeveloperAccessGate,
  } = useDeveloperAccessGate({
    hasAccessToken,
    profileRole,
    tenantId,
    isLoadingIdentity: isLoadingMe,
  });
  const distribution = useMemo(
    () => buildClientDistribution(clients),
    [clients],
  );
  const clientFilterOptions = useMemo<ClientFilterOption[]>(
    () =>
      [...clients]
        .map((client) => ({
          id: client.id,
          label: client.name || client.id,
        }))
        .sort((left, right) => left.label.localeCompare(right.label)),
    [clients],
  );
  const visibleClientIds = useMemo(
    () => clients.map((client) => client.id).filter(Boolean),
    [clients],
  );
  const currentClientIdSet = useMemo(
    () => new Set(visibleClientIds),
    [visibleClientIds],
  );
  const stats = statsQuery.data;
  const usageRows = usageQuery.data?.items ?? [];
  const filteredUsageRows = useMemo(() => {
    if (selectedClientIds.length === 0) {
      return usageRows;
    }
    const selectedSet = new Set(selectedClientIds);
    return usageRows.filter((row) => selectedSet.has(row.clientId));
  }, [selectedClientIds, usageRows]);
  const selectedMultiLineSeries = useMemo(
    () => buildMultiLineSeries(filteredUsageRows),
    [filteredUsageRows],
  );
  const { data: recentAuditData } = useQuery({
    queryKey: [
      "dev-dashboard-audit-logs",
      "clients-recent",
      visibleClientIds.join("|"),
      profileRole,
    ],
    queryFn: async () => {
      const globalLogs = await fetchDevAuditLogs(50);
      if (globalLogs.items.length > 0 || profileRole === "super_admin") {
        return globalLogs;
      }

      if (visibleClientIds.length === 0) {
        return globalLogs;
      }

      const perClientLogs = await Promise.all(
        visibleClientIds.slice(0, 20).map(async (clientId) => {
          try {
            const result = await fetchDevAuditLogs(5, undefined, {
              client_id: clientId,
            });
            return result.items;
          } catch {
            return [];
          }
        }),
      );

      const merged = perClientLogs
        .flat()
        .filter(
          (item, index, self) =>
            self.findIndex(
              (candidate) => candidate.event_id === item.event_id,
            ) === index,
        )
        .sort(
          (left, right) =>
            new Date(right.timestamp).getTime() -
            new Date(left.timestamp).getTime(),
        )
        .slice(0, 50);

      return {
        items: merged,
        limit: 50,
        cursor: globalLogs.cursor,
        next_cursor: globalLogs.next_cursor,
      };
    },
    enabled: hasAccessToken && clients.length > 0 && Boolean(profileRole),
    retry: false,
  });
  const recentClientChanges = useMemo<RecentClientChange[]>(
    () => buildRecentClientChanges(recentAuditData?.items ?? [], clients),
    [clients, recentAuditData?.items],
  );
  const recentClientActorIds = useMemo(
    () =>
      Array.from(
        new Set(
          recentClientChanges
            .map((item) => item.actorId.trim())
            .filter((actorId) => actorId && actorId !== "-"),
        ),
      ),
    [recentClientChanges],
  );
  const { data: recentClientActors } = useQuery({
    queryKey: ["dev-dashboard-recent-client-actors", recentClientActorIds],
    queryFn: async () => {
      const entries = await Promise.all(
        recentClientActorIds.map(async (actorId) => {
          try {
            const user = await fetchDevUser(actorId);
            return [actorId, user.name || actorId] as const;
          } catch {
            return [actorId, actorId] as const;
          }
        }),
      );
      return Object.fromEntries(entries);
    },
    enabled: recentClientActorIds.length > 0,
  });
  const recentClientChangesWithActors = useMemo(
    () =>
      recentClientChanges.map((item) => ({
        ...item,
        actorName: recentClientActors?.[item.actorId] || item.actorId,
      })),
    [recentClientActors, recentClientChanges],
  );
  const deletedRecentChangeClientIds = useMemo(
    () =>
      Array.from(
        new Set(
          recentClientChangesWithActors
            .map((item) => item.clientId)
            .filter((clientId) => !currentClientIdSet.has(clientId)),
        ),
      ),
    [currentClientIdSet, recentClientChangesWithActors],
  );
  const recentChangeFilterOptions = useMemo<ClientFilterOption[]>(() => {
    const activeOptions = Array.from(
      new Map(
        recentClientChangesWithActors
          .filter((item) => currentClientIdSet.has(item.clientId))
          .map((item) => [
            item.clientId,
            { id: item.clientId, label: item.clientName },
          ]),
      ).values(),
    ).sort((left, right) => left.label.localeCompare(right.label));

    if (deletedRecentChangeClientIds.length === 0) {
      return activeOptions;
    }

    return [
      ...activeOptions,
      {
        id: deletedRecentChangeFilterId,
        label: t("ui.dev.dashboard.recent_changes.deleted_group", "삭제된 앱"),
      },
    ];
  }, [
    currentClientIdSet,
    deletedRecentChangeClientIds.length,
    recentClientChangesWithActors,
  ]);
  const filteredRecentClientChanges = useMemo(() => {
    if (selectedRecentChangeClientIds.length === 0) {
      return recentClientChangesWithActors;
    }
    const selectedSet = new Set(selectedRecentChangeClientIds);
    const includeDeletedGroup = selectedSet.has(deletedRecentChangeFilterId);
    return recentClientChangesWithActors.filter(
      (item) =>
        selectedSet.has(item.clientId) ||
        (includeDeletedGroup &&
          deletedRecentChangeClientIds.includes(item.clientId)),
    );
  }, [
    deletedRecentChangeClientIds,
    recentClientChangesWithActors,
    selectedRecentChangeClientIds,
  ]);
  const selectedRecentChangeSeries = useMemo(
    () =>
      buildRecentChangeSeries(filteredRecentClientChanges, recentChangesPeriod),
    [filteredRecentClientChanges, recentChangesPeriod],
  );
  const recentChangedClientCount = useMemo(
    () =>
      new Set(
        filteredRecentClientChanges
          .map((item) => item.clientId)
          .filter((clientId) => currentClientIdSet.has(clientId)),
      ).size,
    [currentClientIdSet, filteredRecentClientChanges],
  );
  const deletedRecentChangedClientCount = useMemo(
    () =>
      new Set(
        filteredRecentClientChanges
          .map((item) => item.clientId)
          .filter((clientId) =>
            deletedRecentChangeClientIds.includes(clientId),
          ),
      ).size,
    [deletedRecentChangeClientIds, filteredRecentClientChanges],
  );
  const recentChangeCount = filteredRecentClientChanges.length;
  const latestRecentChange = filteredRecentClientChanges[0];
  const visibleRecentClientChanges = filteredRecentClientChanges.slice(
    0,
    visibleRecentClientChangesCount,
  );
  const hasMoreRecentClientChanges =
    filteredRecentClientChanges.length > visibleRecentClientChanges.length;
  const isAllRecentChangeClientsSelected =
    selectedRecentChangeClientIds.length === 0;
  const usageError = usageQuery.error as AxiosError<{ error?: string }> | null;
  const usageStatus = usageError?.response?.status;
  const usageErrorMessage =
    usageError?.response?.data?.error ?? usageError?.message ?? "";
  const usageErrorText =
    usageStatus === 403
      ? t(
          "msg.dev.dashboard.chart.forbidden",
          "현재 계정에는 RP 이용 통계를 볼 권한이 없습니다.",
        )
      : usageStatus === 503
        ? t(
            "msg.dev.dashboard.chart.service_unavailable",
            "RP 이용 통계 집계 서비스가 아직 준비되지 않았습니다.",
          )
        : usageStatus === 500
          ? t(
              "msg.dev.dashboard.chart.server_error",
              "RP 이용 통계 조회 중 서버 오류가 발생했습니다.",
            )
          : t(
              "msg.dev.dashboard.chart.unavailable_with_reason",
              "RP 이용 통계 API 응답을 확인할 수 없습니다. {{reason}}",
              {
                reason:
                  usageErrorMessage ||
                  t("err.common.unknown", "알 수 없는 오류"),
              },
            );
  const isAllClientsSelected = selectedClientIds.length === 0;

  const toggleClientSelection = (clientId: string) => {
    setSelectedClientIds((current) => {
      if (current.includes(clientId)) {
        const next = current.filter((item) => item !== clientId);
        return next;
      }
      return [...current, clientId];
    });
  };

  const selectAllClients = () => {
    setSelectedClientIds([]);
  };
  const toggleRecentChangeClientSelection = (clientId: string) => {
    setSelectedRecentChangeClientIds((current) => {
      if (current.includes(clientId)) {
        return current.filter((item) => item !== clientId);
      }
      return [...current, clientId];
    });
  };
  const selectAllRecentChangeClients = () => {
    setSelectedRecentChangeClientIds([]);
  };

  useEffect(() => {
    setVisibleRecentClientChangesCount((current) =>
      Math.min(Math.max(6, current), filteredRecentClientChanges.length),
    );
  }, [filteredRecentClientChanges.length]);

  if (isLoadingDeveloperAccessGate) {
    return (
      <div className="p-8 text-center">
        {t("ui.common.loading", "Loading...")}
      </div>
    );
  }

  if (!hasDeveloperAccess) {
    return (
      <DeveloperAccessRequestCard
        title={t("ui.common.overview.title", "운영 현황")}
        isPending={isDeveloperRequestPending}
        canRequest={canRequestDeveloperAccess}
        pendingMessage={t(
          "msg.dev.dashboard.access_pending",
          "개발자 권한 신청을 검토 중입니다.",
        )}
        pendingDetailMessage={t(
          "msg.dev.dashboard.access_pending_detail",
          "super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다.",
        )}
        deniedMessage={t(
          "msg.dev.dashboard.access_denied",
          "대시보드는 개발자 권한이 있어야 볼 수 있습니다.",
        )}
        deniedDetailMessage={t(
          "msg.dev.dashboard.access_denied_detail",
          "개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요.",
        )}
        actionLabel={t("ui.dev.nav.developer_request", "개발자 권한 신청")}
        onAction={() => navigate("/developer-requests")}
      />
    );
  }

  return (
    <div className="space-y-4 animate-in fade-in duration-500">
      <div className="flex flex-wrap items-start justify-between gap-4">
        <div className="flex min-w-0 items-start gap-3">
          <div className="mt-1 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border border-primary/15 bg-primary/10 text-primary">
            <LayoutDashboard size={20} />
          </div>
          <div className="space-y-1">
            <h2 className="text-3xl font-semibold">
              {t("ui.common.overview.title", "운영 현황")}
            </h2>
            <p className="text-sm text-muted-foreground">
              {t(
                "msg.dev.dashboard.description",
                "연동 앱 구성과 인증 운영 지표를 한 곳에서 확인합니다.",
              )}
            </p>
          </div>
        </div>
      </div>

      <div className="flex flex-wrap items-center gap-x-6 gap-y-2 border-y border-border/60 py-2">
        <OverviewMetric
          icon={<ShieldCheck size={14} />}
          label={t("ui.dev.dashboard.summary.total_clients", "총 RP 수")}
          value={formatMetric(stats?.total_clients ?? clients.length)}
        />
        <OverviewMetric
          icon={<CheckCircle2 size={14} />}
          label={t("ui.dev.dashboard.summary.active_clients", "활성 RP 수")}
          value={formatMetric(distribution.activeClients)}
        />
        <OverviewMetric
          icon={<Activity size={14} />}
          label={t("ui.dev.dashboard.summary.active_sessions", "활성 세션 수")}
          value={formatMetric(stats?.active_sessions)}
        />
        <OverviewMetric
          icon={<AlertTriangle size={14} />}
          label={t(
            "ui.dev.dashboard.summary.auth_failures_24h",
            "24시간 인증 실패 수",
          )}
          value={formatMetric(stats?.auth_failures_24h)}
        />
      </div>

      <section className="space-y-3">
        <div className="flex flex-wrap items-center justify-between gap-3">
          <div className="space-y-1">
            <h3 className="text-lg font-bold">
              {t(
                "ui.dev.dashboard.chart.title",
                "애플리케이션별 로그인요청/기타 요청 현황",
              )}
            </h3>
            <p className="text-sm text-muted-foreground">
              {t(
                "msg.dev.dashboard.chart.filter_description",
                "전체 또는 선택한 애플리케이션만 기준으로 그래프를 확인합니다.",
              )}
            </p>
          </div>
          <fieldset
            className="flex h-8 items-center gap-1"
            aria-label="집계 단위"
          >
            {[
              ["day", t("ui.common.chart.period.day", "일")],
              ["week", t("ui.common.chart.period.week", "주")],
              ["month", t("ui.common.chart.period.month", "월")],
            ].map(([value, label]) => (
              <button
                key={value}
                type="button"
                aria-pressed={period === value}
                onClick={() => setPeriod(value as RPUsagePeriod)}
                className={`h-8 rounded px-3 text-xs font-medium transition-colors ${
                  period === value
                    ? "bg-primary text-primary-foreground"
                    : "bg-muted/60 hover:bg-muted"
                }`}
              >
                {label}
              </button>
            ))}
          </fieldset>
        </div>

        <OverviewSelectionChips
          allLabel={t("ui.dev.dashboard.chart.filter_all", "전체")}
          options={clientFilterOptions}
          selectedIds={selectedClientIds}
          onSelectAll={selectAllClients}
          onToggle={toggleClientSelection}
        />

        {usageQuery.isError ? (
          <div className="text-sm text-muted-foreground">{usageErrorText}</div>
        ) : isAllClientsSelected ? (
          <RPUsageMixedChart rows={filteredUsageRows} period={period} />
        ) : (
          <RPUsageMixedChart
            rows={filteredUsageRows}
            period={period}
            multiLineSeries={selectedMultiLineSeries}
          />
        )}
      </section>

      <section className="space-y-3">
        <div className="flex flex-wrap items-center justify-between gap-3">
          <div className="space-y-1">
            <h3 className="text-lg font-bold">
              {t("ui.dev.dashboard.recent_changes.title", "최근 변경된 앱")}
            </h3>
            <p className="text-sm text-muted-foreground">
              {t(
                "msg.dev.dashboard.recent_changes.description",
                "변경 또는 삭제된 애플리케이션을 대시보드에서 추이를 확인합니다.",
              )}
            </p>
          </div>
          <div className="flex items-center gap-2">
            <fieldset
              className="flex h-8 items-center gap-1"
              aria-label={t(
                "ui.dev.dashboard.recent_changes.period",
                "최근 변경 집계 단위",
              )}
            >
              {[
                ["day", t("ui.common.chart.period.day", "일")],
                ["week", t("ui.common.chart.period.week", "주")],
                ["month", t("ui.common.chart.period.month", "월")],
              ].map(([value, label]) => (
                <button
                  key={value}
                  type="button"
                  aria-pressed={recentChangesPeriod === value}
                  onClick={() => setRecentChangesPeriod(value as RPUsagePeriod)}
                  className={`h-8 rounded px-3 text-xs font-medium transition-colors ${
                    recentChangesPeriod === value
                      ? "bg-primary text-primary-foreground"
                      : "bg-muted/60 hover:bg-muted"
                  }`}
                >
                  {label}
                </button>
              ))}
            </fieldset>
          </div>
        </div>

        <div className="flex flex-wrap items-center gap-x-6 gap-y-2 border-y border-border/60 py-2">
          <OverviewMetric
            icon={<Clock3 size={14} />}
            label={t(
              "ui.dev.dashboard.recent_changes.summary.total_changes",
              "최근 변경 건수",
            )}
            value={recentChangeCount.toLocaleString()}
          />
          <OverviewMetric
            icon={<Layers3 size={14} />}
            label={t(
              "ui.dev.dashboard.recent_changes.summary.changed_clients",
              "변경된 앱 수",
            )}
            value={recentChangedClientCount.toLocaleString()}
          />
          <OverviewMetric
            icon={<Layers3 size={14} />}
            label={t(
              "ui.dev.dashboard.recent_changes.summary.deleted_clients",
              "삭제된 앱 수",
            )}
            value={deletedRecentChangedClientCount.toLocaleString()}
          />
          <OverviewMetric
            icon={<CheckCircle2 size={14} />}
            label={t(
              "ui.dev.dashboard.recent_changes.summary.latest_change",
              "마지막 변경일",
            )}
            value={formatDate(latestRecentChange?.timestamp)}
          />
        </div>

        <OverviewSelectionChips
          allLabel={t("ui.dev.dashboard.chart.filter_all", "전체")}
          options={recentChangeFilterOptions}
          selectedIds={selectedRecentChangeClientIds}
          onSelectAll={selectAllRecentChangeClients}
          onToggle={toggleRecentChangeClientSelection}
        />

        {isAllRecentChangeClientsSelected ? (
          <RecentClientChangesChart
            items={filteredRecentClientChanges}
            period={recentChangesPeriod}
          />
        ) : (
          <RecentClientChangesChart
            items={filteredRecentClientChanges}
            period={recentChangesPeriod}
            multiLineSeries={selectedRecentChangeSeries}
          />
        )}

        <button
          type="button"
          className="mt-8 flex w-full items-center justify-between rounded-xl border border-border/40 bg-card/60 px-4 py-3 text-left transition-colors hover:bg-card/80"
          aria-expanded={isRecentChangesDetailOpen}
          onClick={() => setIsRecentChangesDetailOpen((current) => !current)}
        >
          <span className="text-sm font-medium text-foreground">
            {isRecentChangesDetailOpen
              ? t("ui.common.collapse", "접기")
              : t("ui.common.details", "상세정보")}
          </span>
          <ChevronDown
            size={16}
            className={`text-muted-foreground transition-transform ${
              isRecentChangesDetailOpen ? "rotate-180" : ""
            }`}
          />
        </button>

        {isRecentChangesDetailOpen ? (
          <div className="mt-3 rounded-2xl border border-border/40 bg-card/70 p-4">
            <div className="grid gap-3 lg:grid-cols-2">
              {filteredRecentClientChanges.length === 0 ? (
                <div className="rounded-xl border border-dashed border-border/40 bg-card/80 p-5 text-sm text-muted-foreground lg:col-span-2">
                  {t(
                    "msg.dev.dashboard.recent_changes.empty",
                    "최근 변경 로그가 아직 없습니다.",
                  )}
                </div>
              ) : (
                visibleRecentClientChanges.map((item) => {
                  const { date, time } = formatRecentChangeTimestamp(
                    item.timestamp,
                  );
                  return (
                    <div
                      key={item.eventId}
                      className="rounded-xl border border-border/40 bg-card/80 p-4"
                    >
                      <div className="flex flex-wrap items-center gap-2">
                        <Link
                          to={`/clients/${item.clientId}`}
                          className="font-semibold transition-colors hover:text-primary"
                        >
                          {item.clientName}
                        </Link>
                        <Badge variant="muted">{item.actionLabel}</Badge>
                        <span className="text-xs text-muted-foreground">
                          {item.actorName}
                        </span>
                      </div>
                      <div className="mt-3 flex flex-wrap gap-2">
                        {item.detailLabels.length > 0 ? (
                          item.detailLabels.map((detail) => (
                            <Badge
                              key={`${item.eventId}-${detail.label}`}
                              variant="outline"
                            >
                              {detail.label}: {detail.value}
                            </Badge>
                          ))
                        ) : (
                          <span className="text-sm text-muted-foreground">
                            {t(
                              "msg.dev.clients.recent_changes.no_detail",
                              "변경 항목을 확인할 수 없습니다.",
                            )}
                          </span>
                        )}
                      </div>
                      <p className="mt-3 text-xs text-muted-foreground">
                        {date} {time}
                      </p>
                    </div>
                  );
                })
              )}
              {hasMoreRecentClientChanges ? (
                <div className="pt-2 text-center lg:col-span-2">
                  <Button
                    type="button"
                    variant="outline"
                    onClick={() =>
                      setVisibleRecentClientChangesCount((current) =>
                        Math.min(
                          current + 6,
                          filteredRecentClientChanges.length,
                        ),
                      )
                    }
                  >
                    {t("ui.common.load_more", "더 보기")}
                  </Button>
                </div>
              ) : null}
            </div>
          </div>
        ) : null}
      </section>
    </div>
  );
}

export default GlobalOverviewPage;
