<template>
  <div class="access-logs">
    <section class="access-overview">
      <section class="access-toolbar">
        <div class="access-toolbar-title">
          <h3>访问日志</h3>
        </div>
        <div class="access-toolbar-actions">
          <TimeRangeSelector v-model="timeRangePreset" @change="onTimePresetChange" />
          <span v-if="lastUpdatedAt" class="ctms-updated-at">更新于 {{ formatLastUpdated(lastUpdatedAt) }}</span>
          <el-button size="small" type="primary" :loading="loading" @click="loadData">刷新</el-button>
        </div>
      </section>

      <section class="audit-metrics" aria-label="访问指标">
        <article v-for="card in metricCards" :key="card.label" class="metric-card" :class="card.tone">
          <div class="metric-icon">
            <component :is="card.icon" />
          </div>
          <div class="metric-body">
            <span class="metric-label">{{ card.label }}</span>
            <strong class="metric-value">{{ card.value }}</strong>
          </div>
        </article>
      </section>
    </section>

    <el-alert v-if="loadError" :title="loadError" type="warning" show-icon :closable="false" class="load-alert" />

    <section class="access-log-card">
      <div class="audit-filters">
        <div class="filter-inputs">
          <el-tag
            v-if="trendDrilldownActive"
            closable
            effect="plain"
            size="small"
            type="primary"
            class="trend-drilldown-tag"
            @close="clearTrendDrilldownRange"
          >
            权限趋势下钻{{ drilldownRangeLabel ? ` · ${drilldownRangeLabel}` : "" }}
          </el-tag>
          <el-select v-model="filters.role" placeholder="角色" clearable size="small" class="filter-role" @change="onFilterChange">
            <el-option v-for="role in roleFilterOptions" :key="role.value" :label="role.label" :value="role.value" />
          </el-select>
          <el-select v-model="filters.allowed" placeholder="结果" clearable size="small" class="filter-result" @change="onFilterChange">
            <el-option label="允许" :value="true" />
            <el-option label="拒绝" :value="false" />
          </el-select>
          <el-select v-model="filters.clientType" placeholder="来源" clearable size="small" class="filter-source" @change="onFilterChange">
            <el-option v-for="source in clientSourceOptions" :key="source.value" :label="source.label" :value="source.value" />
          </el-select>
          <el-input v-model="filters.keyword" placeholder="搜索账号、IP、属地、接口或来源" clearable size="small" class="filter-keyword" :prefix-icon="SearchIcon" @input="onKeywordInput" @clear="onFilterChange" @keyup.enter="onFilterChange" />
          <el-button plain size="small" class="filter-reset" @click="resetFilters">重置</el-button>
        </div>

        <div class="table-actions">
          <el-tag effect="plain" size="small" type="info">共 {{ formatNumber(total) }} 条</el-tag>
          <el-button size="small" class="ctms-btn-dialog ctms-btn-capsule-info" :loading="ipRankingLoading" @click="openIpRankingDialog">
            IP访问排行
            <el-icon class="el-icon--right"><HistogramIcon /></el-icon>
          </el-button>
          <el-button size="small" class="ctms-btn-dialog ctms-btn-gradient-blue" :loading="accessLogDialogLoading" @click="openAccessLogDialog">
            访问日志
            <el-icon class="el-icon--right"><DocumentIcon /></el-icon>
          </el-button>
        </div>
      </div>

      <div class="log-surface">
        <div class="table-shell">
          <el-table v-loading="loading" :data="logs" class="access-table" size="small" table-layout="fixed">
            <el-table-column label="时间" width="160">
              <template #default="{ row }">
                <span class="time-cell">{{ formatTerminalTime(row.created_at) }}</span>
              </template>
            </el-table-column>
            <el-table-column label="IP" width="120">
              <template #default="{ row }">
                <span class="ip-cell">{{ row.ip_address || "未知 IP" }}</span>
              </template>
            </el-table-column>
            <el-table-column label="位置" width="110">
              <template #default="{ row }">
                <span class="location-cell">{{ formatIpLocation(row) || "未知位置" }}</span>
              </template>
            </el-table-column>
            <el-table-column label="账号" min-width="160">
              <template #default="{ row }">
                <div class="account-cell">
                  <strong>{{ row.user_name || "未知账号" }}</strong>
                  <span>{{ accountSecondary(row) }}</span>
                </div>
              </template>
            </el-table-column>
            <el-table-column label="请求" min-width="380">
              <template #default="{ row }">
                <div class="request-cell">
                  <strong>
                    <el-tag :type="isSecurityLog(row) ? 'danger' : 'info'" effect="plain" size="small">{{ eventTypeLabel(row) }}</el-tag>
                    <span>{{ requestMain(row) }}</span>
                  </strong>
                  <span>{{ requestSub(row) }}</span>
                </div>
              </template>
            </el-table-column>
            <el-table-column label="耗时" width="100">
              <template #default="{ row }">
                <div class="elapsed-cell">
                  <strong>{{ row.elapsed_ms.toFixed(1) }}ms</strong>
                  <el-tag :type="accessResultTagType(row)" effect="plain" size="small">{{ accessResultLabel(row) }}</el-tag>
                </div>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="64" align="center">
              <template #default="{ row }">
                <el-button link type="primary" size="small" @click="openLogDetail(row)">详情</el-button>
              </template>
            </el-table-column>
          </el-table>
          <div class="logs-pagination">
            <el-pagination
              v-model:current-page="page"
              v-model:page-size="pageSize"
              :page-sizes="[50, 100, 200]"
              :total="total"
              small
              layout="total, sizes, prev, pager, next"
              @size-change="onPageSizeChange"
              @current-change="loadData"
            />
          </div>
        </div>
      </div>
    </section>

    <el-drawer v-model="detailDrawerVisible" direction="rtl" size="520px" class="access-detail-drawer" :show-close="false">
      <template #header>
        <div class="dialog-head">
          <strong>访问详情</strong>
          <div class="dialog-actions">
            <el-button size="small" @click="detailDrawerVisible = false">关闭</el-button>
          </div>
        </div>
      </template>
      <div v-if="selectedLog" class="detail-stack">
        <div class="detail-grid">
          <div class="detail-card">
            <span>账号</span>
            <strong>{{ selectedLog.user_name || "未知用户" }}</strong>
            <small>{{ selectedLog.user_email || selectedLog.user_id }}</small>
          </div>
          <div class="detail-card">
            <span>来源</span>
            <strong>{{ clientSourceLabel(selectedLog) }}</strong>
            <small>{{ clientMeta(selectedLog) }}</small>
          </div>
          <div class="detail-card">
            <span>IP / 属地</span>
            <strong>{{ selectedLog.ip_address || "未知 IP" }}</strong>
            <small>{{ formatIpLocation(selectedLog) || "未知属地" }}</small>
          </div>
          <div class="detail-card">
            <span>结果 / 耗时</span>
            <strong>{{ accessResultLabel(selectedLog) }}</strong>
            <small>{{ selectedLog.elapsed_ms.toFixed(1) }}ms</small>
          </div>
        </div>
        <div class="detail-section detail-section--request">
          <h4>请求概览</h4>
          <p class="detail-request-line">{{ requestSummaryValue(selectedLog) }}</p>
          <div class="detail-field-grid">
            <div v-for="field in requestOverviewFieldItems(selectedLog)" :key="field.label" class="detail-field-item">
              <span>{{ field.label }}</span>
              <strong>{{ field.value }}</strong>
            </div>
          </div>
        </div>
        <div class="detail-section detail-section--raw">
          <h4>原始 HTTP 请求</h4>
          <pre class="detail-raw-line">{{ buildRawRequestBlock(selectedLog) }}</pre>
        </div>
        <el-collapse class="detail-more">
          <el-collapse-item name="metadata" title="更多审计信息">
            <div class="detail-subsection">
              <h5>日志标识</h5>
              <div class="detail-field-grid">
                <div v-for="field in auditMetadataFieldItems(selectedLog)" :key="field.label" class="detail-field-item">
                  <span>{{ field.label }}</span>
                  <strong>{{ field.value }}</strong>
                </div>
              </div>
            </div>
            <div class="detail-subsection">
              <h5>传输信息</h5>
              <div class="detail-field-grid">
                <div v-for="field in requestSnapshotFieldItems(selectedLog)" :key="field.label" class="detail-field-item">
                  <span>{{ field.label }}</span>
                  <strong>{{ field.value }}</strong>
                </div>
              </div>
            </div>
            <div class="detail-subsection">
              <h5>客户端标识</h5>
              <div class="detail-field-grid">
                <div class="detail-field-item">
                  <span>User-Agent</span>
                  <strong>{{ selectedLog.user_agent || "未记录" }}</strong>
                </div>
                <div class="detail-field-item">
                  <span>构建信息</span>
                  <strong>{{ buildMeta(selectedLog) }}</strong>
                </div>
              </div>
            </div>
          </el-collapse-item>
        </el-collapse>
      </div>
    </el-drawer>

    <el-dialog v-model="accessLogDialogVisible" title="访问日志" width="82%" @opened="scrollAccessTerminalToBottom">
      <template #header>
        <div class="dialog-head">
          <strong>访问日志</strong>
          <div class="dialog-actions">
            <el-tag effect="plain" type="info">共 {{ formatNumber(total) }} 条</el-tag>
            <el-button size="small" type="primary" :loading="rawExportLoading" @click="downloadInterfaceLog">下载日志</el-button>
          </div>
        </div>
      </template>
      <div ref="accessTerminalWindowRef" v-loading="accessLogDialogLoading" class="terminal-window dialog-terminal-window">
        <el-alert v-if="accessDialogError" :title="accessDialogError" type="warning" show-icon :closable="false" />
        <pre v-if="accessDialogLines.length" class="terminal-lines"><code>{{ accessDialogLines.join("\n") }}</code></pre>
        <el-empty v-else description="暂无原始访问日志" :image-size="72" />
      </div>
    </el-dialog>

    <el-dialog v-model="ipRankingDialogVisible" title="IP访问排行" width="min(1180px, 96vw)">
      <template #header>
        <div class="dialog-head">
          <strong>IP访问排行</strong>
          <div class="dialog-actions">
            <TimeRangeSelector v-model="ipRankingPeriod" class="ranking-period-cards" @change="selectIpRankingPeriod" />
            <el-tag effect="plain" :type="ipRankingSummary.riskCount > 0 ? 'danger' : 'success'">
              风险 {{ formatNumber(ipRankingSummary.riskCount) }}
            </el-tag>
          </div>
        </div>
      </template>
      <div v-loading="ipRankingLoading" class="ip-ranking-dialog">
        <el-alert v-if="ipRankingError" :title="ipRankingError" type="warning" show-icon :closable="false" />
        <div class="ip-ranking-list-head">
          <el-tag effect="plain" size="small" type="info">TOP {{ ipRankingRows.length }}</el-tag>
          <span>
            访问 {{ formatNumber(ipRankingSummary.totalCount) }} · 风险 {{ formatNumber(ipRankingSummary.riskCount) }} · 高危 {{ formatNumber(ipRankingSummary.highRiskCount) }} · IP {{ formatNumber(ipRankingSummary.uniqueIpCount) }}
          </span>
        </div>
        <div v-if="ipRankingRows.length" class="ip-rank-grid">
          <article v-for="(row, index) in ipRankingRows" :key="row.ip" class="ip-rank-card" :class="riskRankCardClass(row)">
            <div class="rank-card-head">
              <span class="rank-no" :class="{ 'rank-top': index < 3 }">{{ String(index + 1).padStart(2, "0") }}</span>
              <el-tag v-if="row.riskCount > 0" size="small" :type="row.riskTagType" effect="plain" class="rank-risk-tag">{{ row.riskLabel }}</el-tag>
            </div>
            <div class="rank-card-ip">
              <strong>{{ row.ip }}</strong>
              <span>{{ row.location }}</span>
            </div>
            <div class="rank-card-meta">
              <span>{{ row.latestAccount }}</span>
              <small>{{ row.latestRole }}</small>
            </div>
            <div class="rank-card-stats">
              <div>
                <span>访问</span>
                <strong>{{ formatNumber(row.total) }}</strong>
              </div>
              <div>
                <span>异常/拒绝</span>
                <strong :class="{ 'denied-highlight': row.riskCount > 0 }">{{ formatNumber(row.riskCount) }}</strong>
              </div>
            </div>
          </article>
        </div>
        <el-empty v-else description="当前时段暂无风险访问" :image-size="72" />
      </div>
    </el-dialog>

  </div>
</template>

<script setup lang="ts">
import { computed, h, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { fetchAccessLogs } from "@/api/projectPermissions";
import type {
  AccessLogItem,
  AccessLogsSummary,
} from "@/types/api";
import { Search as SearchIcon, Document as DocumentIcon, Histogram as HistogramIcon } from "@element-plus/icons-vue";
import { useRoleTemplateMeta } from "@/composables/useRoleTemplateMeta";
import { saveFileWithFeedback } from "@/utils/fileTaskFeedback";
import TimeRangeSelector from "./TimeRangeSelector.vue";

const { roleLabel, roleOptionsFor, loadRoleTemplates } = useRoleTemplateMeta();

const MetricIconVisit = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [
  h("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
  h("circle", { cx: "12", cy: "12", r: "3" }),
]);
const MetricIconUsers = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [
  h("path", { d: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" }),
  h("circle", { cx: "9", cy: "7", r: "4" }),
  h("path", { d: "M23 21v-2a4 4 0 0 0-3-3.87" }),
  h("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" }),
]);
const MetricIconDeny = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [
  h("circle", { cx: "12", cy: "12", r: "10" }),
  h("line", { x1: "4.93", y1: "4.93", x2: "19.07", y2: "19.07" }),
]);
const MetricIconSpeed = () => h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, [
  h("circle", { cx: "12", cy: "12", r: "10" }),
  h("polyline", { points: "12 6 12 12 16 14" }),
]);

const REALTIME_POLL_INTERVAL_MS = 30_000;
const ACCESS_LOG_EXPORT_PAGE_SIZE = 200;
const IP_RANKING_PAGE_SIZE = 200;
const IP_RANKING_LIMIT = 10;

const TIME_RANGE_PRESETS = [
  { value: "all", label: "全部", hours: null },
  { value: "24h", label: "24小时", hours: 24 },
  { value: "7d", label: "7天", hours: 24 * 7 },
  { value: "30d", label: "30天", hours: 24 * 30 },
] as const;

type TimeRangePreset = (typeof TIME_RANGE_PRESETS)[number]["value"];
type AccessTimeRangePreset = TimeRangePreset | "custom";

interface TrendDrilldownPayload {
  preset?: Exclude<TimeRangePreset, "all">;
  startAt?: string;
  endAt?: string;
  allowed?: boolean;
  role?: string;
  clientType?: string;
  keyword?: string;
}

interface IpRankingRow {
  ip: string;
  location: string;
  total: number;
  riskCount: number;
  highRiskCount: number;
  lastSeenAt: string;
  latestRequest: string;
  latestAccount: string;
  latestRole: string;
  categoryText: string;
  riskLabel: string;
  riskTagType: "danger" | "warning" | "info";
}

interface IpRiskDescriptor {
  label: string;
  tagType: "danger" | "warning" | "info";
  priority: number;
}

interface IpRankingSummary {
  totalCount: number;
  riskCount: number;
  highRiskCount: number;
  uniqueIpCount: number;
}

type IpRankingAccumulator = IpRankingRow & {
  lastSeenAtMs: number;
  riskPriority: number;
};

const emptySummary: AccessLogsSummary = {
  total_count: 0,
  unique_user_count: 0,
  unique_ip_count: 0,
  denied_count: 0,
  avg_elapsed_ms: 0,
};

const emptyIpRankingSummary: IpRankingSummary = {
  totalCount: 0,
  riskCount: 0,
  highRiskCount: 0,
  uniqueIpCount: 0,
};

const loading = ref(false);
const rawExportLoading = ref(false);
const accessLogDialogLoading = ref(false);
const ipRankingLoading = ref(false);
const logs = ref<AccessLogItem[]>([]);
const accessDialogLines = ref<string[]>([]);
const ipRankingRows = ref<IpRankingRow[]>([]);
const ipRankingSummary = ref<IpRankingSummary>({ ...emptyIpRankingSummary });
const summary = ref<AccessLogsSummary>(emptySummary);
const total = ref(0);
const page = ref(1);
const pageSize = ref(50);
const timeRangePreset = ref<AccessTimeRangePreset>("24h");
const ipRankingPeriod = ref<TimeRangePreset>("24h");
const trendDrilldownRange = ref<[Date, Date] | null>(null);
const trendDrilldownActive = ref(false);
const accessLogDialogVisible = ref(false);
const ipRankingDialogVisible = ref(false);
const accessTerminalWindowRef = ref<HTMLElement | null>(null);
const realtimeTimer = ref<number | null>(null);
const keywordTimer = ref<number | null>(null);
const detailDrawerVisible = ref(false);
const selectedLog = ref<AccessLogItem | null>(null);
const loadError = ref("");
const lastUpdatedAt = ref<Date | null>(null);
const accessDialogError = ref("");
const ipRankingError = ref("");

const filters = reactive({
  role: undefined as string | undefined,
  allowed: undefined as boolean | undefined,
  clientType: undefined as string | undefined,
  keyword: "",
});

const ROLE_FILTER_KEYS = ["ADMIN", "PM", "CRA", "PV", "QA", "CTA"];
const roleFilterOptions = computed(() => roleOptionsFor(ROLE_FILTER_KEYS));
const timeRangePresetOptions = TIME_RANGE_PRESETS;
const ipRankingPeriodOptions = TIME_RANGE_PRESETS;
const selectedIpRankingPeriod = computed(() => TIME_RANGE_PRESETS.find((period) => period.value === ipRankingPeriod.value) || TIME_RANGE_PRESETS[1]);
const clientSourceOptions = [
  { value: "web", label: "网页端" },
  { value: "desktop", label: "桌面端" },
  { value: "unknown", label: "未知/异常" },
];

const SECURITY_CATEGORY_LABELS: Record<string, string> = {
  ABNORMAL_IP: "异常IP",
  PROBE: "敏感路径探测",
  INVALID_TOKEN: "无效令牌",
  SERVER_ERROR: "服务异常",
  ANONYMOUS_API: "匿名 API",
  NOT_FOUND_NOISE: "普通 404",
  OTHER: "其他",
};

const SECURITY_SEVERITY_LABELS: Record<string, string> = {
  CRITICAL: "严重",
  HIGH: "高",
  MEDIUM: "中",
  LOW: "低",
};

const AUTH_STATUS_LABELS: Record<string, string> = {
  ANONYMOUS: "匿名",
  INVALID_TOKEN: "无效令牌",
  AUTHENTICATED: "已认证",
};

const formatTerminalTime = (iso: string) => {
  const date = new Date(iso);
  const pad = (value: number) => String(value).padStart(2, "0");
  return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};

const formatIpLocation = (row: AccessLogItem) => {
  const parts = [row.ip_province, row.ip_city, row.ip_isp].filter(Boolean);
  return parts.length ? parts.join(" / ") : row.ip_location;
};

const isSecurityLog = (row: AccessLogItem) => row.event_type === "security";

const eventTypeLabel = (row: AccessLogItem) => (isSecurityLog(row) ? "安全事件" : "业务访问");

const securityCategoryLabel = (value?: string | null) => SECURITY_CATEGORY_LABELS[value || ""] || value || "其他";

const securitySeverityLabel = (value?: string | null) => SECURITY_SEVERITY_LABELS[value || ""] || value || "-";

const authStatusLabel = (value?: string | null) => AUTH_STATUS_LABELS[value || ""] || value || "-";

const isScriptLikeUserAgent = (userAgent?: string | null) => {
  const ua = String(userAgent || "").toLowerCase();
  return ["curl", "wget", "python-requests", "go-http-client", "java/", "okhttp", "postman", "insomnia", "httpie"].some((marker) =>
    ua.includes(marker),
  );
};

const clientSourceLabel = (row: AccessLogItem) => {
  const clientType = String(row.client_type || "").toLowerCase();
  if (clientType === "web") return "网页端";
  if (clientType === "desktop") return "桌面端";
  if (isScriptLikeUserAgent(row.user_agent)) return "脚本/自动化";
  return "未知/异常";
};

const clientMeta = (row: AccessLogItem) => {
  const parts = [row.client_type, row.client_version, row.client_platform].filter(Boolean);
  if (parts.length) return parts.join(" / ");
  return row.user_agent ? "未标识客户端" : "未记录来源";
};

const buildMeta = (row: AccessLogItem) => [row.build_channel, row.build_commit].filter(Boolean).join(" / ") || "未记录";

const requestSnapshot = (row: AccessLogItem) => row.request_snapshot || {};

const hasCapturedRequestSnapshot = (row: AccessLogItem) => Object.keys(requestSnapshot(row)).length > 0;

const isLegacyPermissionLog = (row: AccessLogItem) => !isSecurityLog(row) && !hasCapturedRequestSnapshot(row) && !row.method && !row.path;

const requestHeaderEntries = (row: AccessLogItem) =>
  Object.entries(requestSnapshot(row).headers || row.request_headers || {}).sort(([left], [right]) => left.localeCompare(right));

const requestQueryString = (row: AccessLogItem) => requestSnapshot(row).query_string || "";

const requestMethodValue = (row: AccessLogItem) => requestSnapshot(row).method || row.method || (isSecurityLog(row) ? "HTTP" : "历史未采集");

const requestPathValue = (row: AccessLogItem) => requestSnapshot(row).path || row.path || (isSecurityLog(row) ? row.endpoint_key : `权限点:${row.endpoint_key}`);

const requestTargetValue = (row: AccessLogItem) => {
  const path = requestPathValue(row);
  const query = requestQueryString(row);
  if (!query || path.includes("?")) return path;
  return `${path}?${query}`;
};

const requestSummaryValue = (row: AccessLogItem) => {
  if (isLegacyPermissionLog(row)) return `历史日志未采集原始请求；权限点 ${row.endpoint_key}`;
  return `${requestMethodValue(row)} ${requestTargetValue(row)}`.trim();
};

const rawRequestText = (row: AccessLogItem) => {
  if (isLegacyPermissionLog(row)) return `legacy_permission_endpoint=${row.endpoint_key} snapshot=历史日志未采集原始请求`;
  return `method=${requestMethodValue(row)} path=${requestTargetValue(row)}`;
};

const accountSecondary = (row: AccessLogItem) => row.user_email || row.user_id || row.account_label || "未知账号";

const endpointTitle = (row: AccessLogItem) => {
  if (isSecurityLog(row)) return `${requestMethodValue(row)} ${requestTargetValue(row)}`.trim();
  return row.endpoint_key;
};

const endpointMeta = (row: AccessLogItem) => {
  if (isSecurityLog(row)) {
    const parts = [securityCategoryLabel(row.category || row.role), row.status_code ? `HTTP ${row.status_code}` : "", securitySeverityLabel(row.severity)].filter(Boolean);
    return parts.join(" / ");
  }
  return roleLabel(row.role);
};

const requestMain = (row: AccessLogItem) => {
  if (isSecurityLog(row)) {
    const method = row.method || "HTTP";
    const status = row.status_code ? `${row.status_code}` : "-";
    return `${method} ${status} / ${securityCategoryLabel(row.category || row.role)}`;
  }
  return endpointTitle(row);
};

const requestSub = (row: AccessLogItem) => {
  if (isSecurityLog(row)) return requestTargetValue(row);
  return `${roleLabel(row.role)} / ${clientSourceLabel(row)} / ${clientMeta(row)}`;
};

const accessResultLabel = (row: AccessLogItem) => {
  if (isSecurityLog(row)) return row.status_code ? `HTTP ${row.status_code}` : "安全事件";
  return row.allowed ? "允许" : "拒绝";
};

const accessResultTagType = (row: AccessLogItem) => {
  if (isSecurityLog(row)) return row.status_code && row.status_code >= 500 ? "danger" : "warning";
  return row.allowed ? "success" : "danger";
};

const requestOverviewFieldItems = (row: AccessLogItem) => {
  const fields = [
    { label: "时间", value: formatTerminalTime(row.created_at) },
    { label: isSecurityLog(row) ? "接口类型" : "权限点", value: endpointTitle(row) },
    { label: "角色/分类", value: endpointMeta(row) },
  ];
  if (!isSecurityLog(row)) return fields;
  return [
    ...fields,
    { label: "认证状态", value: authStatusLabel(row.auth_status) },
    { label: "风险等级", value: securitySeverityLabel(row.severity) },
  ];
};

const auditMetadataFieldItems = (row: AccessLogItem) => [
  { label: "日志ID", value: row.id },
  { label: "类型", value: eventTypeLabel(row) },
  { label: "项目ID", value: row.study_id || "无项目" },
  { label: "用户ID", value: row.user_id || "未知" },
];

const requestSnapshotFieldItems = (row: AccessLogItem) => {
  if (!hasCapturedRequestSnapshot(row)) {
    return [
      { label: "采集状态", value: "历史日志未采集原始请求快照" },
      { label: "可用请求信息", value: isSecurityLog(row) ? `${requestMethodValue(row)} ${requestTargetValue(row)}`.trim() : row.endpoint_key },
      { label: "说明", value: "该记录创建于请求快照能力上线前，无法还原 Method/Path/Query/Headers 原文" },
      { label: "Body", value: "未采集明文" },
    ];
  }
  const snapshot = requestSnapshot(row);
  const client = snapshot.client || {};
  const content = snapshot.content || {};
  const body = snapshot.body || {};
  return [
    { label: "HTTP Version", value: snapshot.http_version || "未记录" },
    { label: "Scheme", value: snapshot.scheme || "未记录" },
    { label: "Client Socket", value: [client.host, client.port].filter(Boolean).join(":") || "未记录" },
    { label: "Content-Type", value: content.type || "未记录" },
    { label: "Content-Length", value: content.length || "未记录" },
    { label: "Body", value: body.captured ? "已采集" : "未采集明文" },
    { label: "Body Policy", value: body.reason || "body_not_captured_by_audit_policy" },
  ];
};

const rawRequestBodyLine = (row: AccessLogItem) => {
  const body = requestSnapshot(row).body || {};
  if (body.captured) return "[request body captured]";
  return `[request body not captured: ${body.reason || "body_not_captured_by_audit_policy"}]`;
};

const buildRawRequestBlock = (row: AccessLogItem) => {
  if (isLegacyPermissionLog(row)) {
    return [
      `# 历史日志未采集原始 HTTP 请求`,
      `# permission_endpoint: ${row.endpoint_key}`,
      `# created_at: ${formatTerminalTime(row.created_at)}`,
      `# audit_line: ${buildTerminalLine(row)}`,
    ].join("\n");
  }

  const version = requestSnapshot(row).http_version ? `HTTP/${requestSnapshot(row).http_version}` : "HTTP";
  const lines = [`${requestMethodValue(row)} ${requestTargetValue(row)} ${version}`.trim()];
  const headers = requestHeaderEntries(row);
  if (headers.length) {
    headers.forEach(([name, value]) => {
      lines.push(`${name}: ${value}`);
    });
  } else if (row.user_agent) {
    lines.push(`user-agent: ${row.user_agent}`);
  }
  lines.push("");
  lines.push(rawRequestBodyLine(row));
  return lines.join("\n");
};

const formatNumber = (value: number) => new Intl.NumberFormat("zh-CN").format(value || 0);
const formatLastUpdated = (value: Date) => {
  return `${value.getFullYear()}/${value.getMonth() + 1}/${value.getDate()} ${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}:${String(value.getSeconds()).padStart(2, "0")}`;
};

const buildTimeRangeFromPreset = (preset: AccessTimeRangePreset): [Date, Date] | null => {
  if (preset === "custom") return null;
  const option = TIME_RANGE_PRESETS.find((item) => item.value === preset) || TIME_RANGE_PRESETS[1];
  if (!option.hours) return null;
  const end = new Date();
  const start = new Date(end.getTime() - option.hours * 60 * 60 * 1000);
  return [start, end];
};

const metricCards = computed(() => [
  {
    label: "访问次数",
    value: formatNumber(summary.value.total_count),
    tone: "blue",
    icon: MetricIconVisit,
  },
  {
    label: "访问用户数",
    value: formatNumber(summary.value.unique_user_count),
    tone: "cyan",
    icon: MetricIconUsers,
  },
  {
    label: "拒绝次数",
    value: formatNumber(summary.value.denied_count),
    tone: summary.value.denied_count > 0 ? "danger" : "violet",
    icon: MetricIconDeny,
  },
  {
    label: "平均耗时",
    value: `${summary.value.avg_elapsed_ms.toFixed(1)}ms`,
    tone: "indigo",
    icon: MetricIconSpeed,
  },
]);

const buildTerminalLine = (row: AccessLogItem) => {
  if (isSecurityLog(row)) {
    const ip = row.ip_address || "-";
    const location = formatIpLocation(row) || "未知属地";
    const userAgent = row.user_agent || "-";
    return `${formatTerminalTime(row.created_at)} [SECURITY:${securitySeverityLabel(row.severity)}] status=${row.status_code || "-"} category=${securityCategoryLabel(row.category)} auth=${authStatusLabel(row.auth_status)} account=${row.account_label || row.user_name || "未知账号"} source=${clientSourceLabel(row)} client=${clientMeta(row)} ip=${ip} loc=${location} ${rawRequestText(row)} ua=${userAgent} cost=${row.elapsed_ms.toFixed(1)}ms`;
  }
  const result = row.allowed ? "ALLOW" : "DENY ";
  const user = row.user_name || "未知用户";
  const ip = row.ip_address || "-";
  const location = formatIpLocation(row) || "未知属地";
  return `${formatTerminalTime(row.created_at)} [${result}] source=${clientSourceLabel(row)} client=${clientMeta(row)} role=${roleLabel(row.role)} user=${user} account=${row.user_email || row.user_id} ip=${ip} loc=${location} endpoint=${row.endpoint_key} ${rawRequestText(row)} cost=${row.elapsed_ms.toFixed(1)}ms`;
};

const onFilterChange = () => {
  page.value = 1;
  loadData();
};

const onTimePresetChange = () => {
  trendDrilldownRange.value = null;
  onFilterChange();
};

const onKeywordInput = () => {
  if (keywordTimer.value) window.clearTimeout(keywordTimer.value);
  keywordTimer.value = window.setTimeout(() => {
    keywordTimer.value = null;
    onFilterChange();
  }, 350);
};

const resetFilters = () => {
  timeRangePreset.value = "24h";
  trendDrilldownRange.value = null;
  trendDrilldownActive.value = false;
  filters.role = undefined;
  filters.allowed = undefined;
  filters.clientType = undefined;
  filters.keyword = "";
  onFilterChange();
};

const onPageSizeChange = () => {
  page.value = 1;
  loadData();
};

const buildQueryParams = (overrides: Record<string, any> = {}) => {
  const params: Record<string, any> = {
    page: page.value,
    page_size: pageSize.value,
  };
  if (filters.role) params.role = filters.role;
  if (filters.allowed !== undefined) params.allowed = filters.allowed;
  if (filters.clientType) params.client_type = filters.clientType;
  if (trendDrilldownActive.value) params.event_type = "permission";
  const keywordToken = filters.keyword.trim();
  if (keywordToken) {
    params.keyword = keywordToken;
    if (/^[0-9a-f:.]+$/i.test(keywordToken)) params.client_ip = keywordToken;
  }
  const presetRange = trendDrilldownRange.value || buildTimeRangeFromPreset(timeRangePreset.value);
  if (presetRange) {
    params.start_time = presetRange[0].toISOString();
    params.end_time = presetRange[1].toISOString();
  }
  return { ...params, ...overrides };
};

const scrollAccessTerminalToBottom = () => {
  nextTick(() => {
    const terminal = accessTerminalWindowRef.value;
    if (!terminal) return;
    terminal.scrollTop = terminal.scrollHeight;
  });
};

const loadData = async (options: { silent?: boolean } = {}) => {
  if (!options.silent) loading.value = true;
  try {
    const res = await fetchAccessLogs(buildQueryParams());
    logs.value = res.data.items;
    total.value = res.data.total;
    summary.value = res.data.summary || emptySummary;
    loadError.value = "";
    lastUpdatedAt.value = new Date();
  } catch {
    loadError.value = "访问日志加载失败，表格中可能是上次成功获取的数据";
  } finally {
    loading.value = false;
  }
};

const stopRealtimePolling = () => {
  if (!realtimeTimer.value) return;
  window.clearInterval(realtimeTimer.value);
  realtimeTimer.value = null;
};

const stopTimers = () => {
  stopRealtimePolling();
  if (keywordTimer.value) {
    window.clearTimeout(keywordTimer.value);
    keywordTimer.value = null;
  }
};

const startRealtimePolling = () => {
  stopRealtimePolling();
  if (document.visibilityState !== "visible") return;
  realtimeTimer.value = window.setInterval(() => {
    loadData({ silent: true });
  }, REALTIME_POLL_INTERVAL_MS);
};

const onVisibilityChange = () => {
  if (document.visibilityState === "visible") {
    loadData({ silent: true });
    startRealtimePolling();
  } else {
    stopRealtimePolling();
  }
};

const refresh = () => {
  loadData();
};

const drilldownRangeLabel = computed(() => {
  if (!trendDrilldownRange.value) return "";
  const [start, end] = trendDrilldownRange.value;
  const compact = (value: Date) => `${value.getMonth() + 1}/${value.getDate()} ${String(value.getHours()).padStart(2, "0")}:${String(value.getMinutes()).padStart(2, "0")}`;
  return `${compact(start)}–${compact(end)}`;
});

const clearTrendDrilldownRange = () => {
  trendDrilldownRange.value = null;
  trendDrilldownActive.value = false;
  timeRangePreset.value = "24h";
  onFilterChange();
};

const applyTrendFilter = async (payload: TrendDrilldownPayload) => {
  page.value = 1;
  trendDrilldownActive.value = true;
  filters.allowed = payload.allowed;
  filters.role = payload.role;
  filters.clientType = payload.clientType;
  filters.keyword = payload.keyword || "";
  if (payload.startAt && payload.endAt) {
    trendDrilldownRange.value = [new Date(payload.startAt), new Date(payload.endAt)];
    timeRangePreset.value = "custom";
  } else {
    trendDrilldownRange.value = null;
    timeRangePreset.value = payload.preset || "24h";
  }
  await loadData();
};

const openLogDetail = (row: AccessLogItem) => {
  selectedLog.value = row;
  detailDrawerVisible.value = true;
};

const openAccessLogDialog = async () => {
  accessLogDialogVisible.value = true;
  accessLogDialogLoading.value = true;
  accessDialogLines.value = [];
  accessDialogError.value = "";
  try {
    accessDialogLines.value = await fetchAllRawAccessLogLines();
    scrollAccessTerminalToBottom();
  } catch {
    accessDialogError.value = "访问日志加载失败，请稍后重试";
  } finally {
    accessLogDialogLoading.value = false;
  }
};

const downloadLogFile = async (fileName: string, lines: string[]) => {
  const blob = new Blob([`${lines.join("\n")}\n`], { type: "text/plain;charset=utf-8" });
  await saveFileWithFeedback(
    { suggestedName: fileName, mimeType: "text/plain;charset=utf-8", data: blob },
    {
      kind: "export",
      title: "导出日志",
      completedDetail: "日志文件已保存",
    },
  );
};

const fetchAllRawAccessLogLines = async () => {
  const first = await fetchAccessLogs(buildQueryParams({ page: 1, page_size: ACCESS_LOG_EXPORT_PAGE_SIZE }));
  const items = [...first.data.items];
  const totalPages = Math.ceil(first.data.total / ACCESS_LOG_EXPORT_PAGE_SIZE);
  for (let nextPage = 2; nextPage <= totalPages; nextPage += 1) {
    const res = await fetchAccessLogs(buildQueryParams({ page: nextPage, page_size: ACCESS_LOG_EXPORT_PAGE_SIZE }));
    items.push(...res.data.items);
  }
  return items
    .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
    .map(buildTerminalLine);
};

const ipRankingTimeRange = () => {
  const range = buildTimeRangeFromPreset(selectedIpRankingPeriod.value.value);
  if (!range) return {};
  const [start, end] = range;
  return {
    start_time: start.toISOString(),
    end_time: end.toISOString(),
  };
};

const isRiskAccessRow = (row: AccessLogItem) => isSecurityLog(row) || row.allowed === false;

const isHighRiskAccessRow = (row: AccessLogItem) => {
  const severity = String(row.severity || "").toUpperCase();
  return severity === "HIGH" || severity === "CRITICAL" || Boolean(isSecurityLog(row) && row.status_code && row.status_code >= 500);
};

const accessRowCategoryText = (row: AccessLogItem) => {
  if (isSecurityLog(row)) return `${securityCategoryLabel(row.category || row.role)} / ${securitySeverityLabel(row.severity)}`;
  return row.allowed ? "业务访问" : "权限拒绝";
};

const riskDescriptorForRow = (row: AccessLogItem): IpRiskDescriptor => {
  if (!isRiskAccessRow(row)) return { label: "", tagType: "info", priority: 0 };
  if (!isSecurityLog(row)) return { label: "权限拒绝", tagType: "warning", priority: 30 };

  const category = String(row.category || row.role || "").toUpperCase();
  if (category === "ABNORMAL_IP") return { label: "异常IP", tagType: "danger", priority: 90 };
  if (category === "PROBE") return { label: "敏感探测", tagType: "danger", priority: 80 };
  if (category === "SERVER_ERROR") return { label: "服务异常", tagType: "danger", priority: 75 };
  if (category === "INVALID_TOKEN") return { label: "无效令牌", tagType: "warning", priority: 70 };
  if (category === "ANONYMOUS_API") return { label: "匿名访问", tagType: "warning", priority: 60 };
  if (category === "NOT_FOUND_NOISE") return { label: "普通404", tagType: "info", priority: 20 };
  if (row.status_code && row.status_code >= 500) return { label: "服务异常", tagType: "danger", priority: 75 };
  return { label: "安全事件", tagType: "warning", priority: 50 };
};

const accessRowRoleText = (row: AccessLogItem) => {
  if (isSecurityLog(row)) return securityCategoryLabel(row.category || row.role);
  return roleLabel(row.role);
};

const accessRowRequestText = (row: AccessLogItem) => (isSecurityLog(row) ? requestSummaryValue(row) : endpointTitle(row));

const accountDisplayText = (row: AccessLogItem) => {
  const primary = row.user_name || row.account_label || "未知账号";
  const secondary = accountSecondary(row);
  if (!secondary || secondary === primary) return primary;
  return `${primary} / ${secondary}`;
};

const riskRankCardClass = (row: IpRankingRow) => {
  if (row.riskCount <= 0) return [];
  return ["risk-rank-card", `risk-rank-card--${row.riskTagType}`];
};

const fetchAccessRowsForIpRanking = async () => {
  const range = ipRankingTimeRange();
  const first = await fetchAccessLogs({ ...range, page: 1, page_size: IP_RANKING_PAGE_SIZE });
  const items = [...first.data.items];
  const totalPages = Math.ceil(first.data.total / IP_RANKING_PAGE_SIZE);
  for (let nextPage = 2; nextPage <= totalPages; nextPage += 1) {
    const res = await fetchAccessLogs({ ...range, page: nextPage, page_size: IP_RANKING_PAGE_SIZE });
    items.push(...res.data.items);
  }
  return items;
};

const buildIpRankingRows = (items: AccessLogItem[]) => {
  const byIp = new Map<string, IpRankingAccumulator>();
  const summaryCounts: IpRankingSummary = {
    totalCount: items.length,
    riskCount: 0,
    highRiskCount: 0,
    uniqueIpCount: 0,
  };

  items.forEach((item) => {
    const ip = item.ip_address || "未知 IP";
    const seenAtMs = Number.isFinite(new Date(item.created_at).getTime()) ? new Date(item.created_at).getTime() : 0;
    const isRisk = isRiskAccessRow(item);
    const isHighRisk = isHighRiskAccessRow(item);
    const riskDescriptor = riskDescriptorForRow(item);
    if (isRisk) summaryCounts.riskCount += 1;
    if (isHighRisk) summaryCounts.highRiskCount += 1;

    const existing = byIp.get(ip) || {
      ip,
      location: formatIpLocation(item) || "未知位置",
      total: 0,
      riskCount: 0,
      highRiskCount: 0,
      lastSeenAt: item.created_at,
      lastSeenAtMs: 0,
      latestRequest: accessRowRequestText(item),
      latestAccount: accountDisplayText(item),
      latestRole: accessRowRoleText(item),
      categoryText: accessRowCategoryText(item),
      riskLabel: "",
      riskTagType: "info",
      riskPriority: 0,
    };

    existing.total += 1;
    if (isRisk) existing.riskCount += 1;
    if (isHighRisk) existing.highRiskCount += 1;
    if (riskDescriptor.priority > existing.riskPriority) {
      existing.riskLabel = riskDescriptor.label;
      existing.riskTagType = riskDescriptor.tagType;
      existing.riskPriority = riskDescriptor.priority;
    }
    if (seenAtMs >= existing.lastSeenAtMs) {
      existing.location = formatIpLocation(item) || existing.location;
      existing.lastSeenAt = item.created_at;
      existing.lastSeenAtMs = seenAtMs;
      existing.latestRequest = accessRowRequestText(item);
      existing.latestAccount = accountDisplayText(item);
      existing.latestRole = accessRowRoleText(item);
      existing.categoryText = accessRowCategoryText(item);
    }
    byIp.set(ip, existing);
  });

  summaryCounts.uniqueIpCount = byIp.size;
  const rows = Array.from(byIp.values())
    .sort((left, right) => {
      if (right.riskCount !== left.riskCount) return right.riskCount - left.riskCount;
      if (right.highRiskCount !== left.highRiskCount) return right.highRiskCount - left.highRiskCount;
      if (right.total !== left.total) return right.total - left.total;
      return right.lastSeenAtMs - left.lastSeenAtMs;
    })
    .slice(0, IP_RANKING_LIMIT)
    .map(({ lastSeenAtMs: _lastSeenAtMs, riskPriority: _riskPriority, ...row }) => row);

  return { rows, summary: summaryCounts };
};

const loadIpRankingData = async () => {
  ipRankingLoading.value = true;
  ipRankingError.value = "";
  try {
    const ranking = buildIpRankingRows(await fetchAccessRowsForIpRanking());
    ipRankingRows.value = ranking.rows;
    ipRankingSummary.value = ranking.summary;
  } catch {
    ipRankingError.value = "IP 访问排行加载失败，当前内容可能是上次成功结果";
  } finally {
    ipRankingLoading.value = false;
  }
};

const selectIpRankingPeriod = async (period: any) => {
  ipRankingPeriod.value = period as TimeRangePreset;
  await loadIpRankingData();
};

const openIpRankingDialog = async () => {
  ipRankingDialogVisible.value = true;
  await loadIpRankingData();
};

const downloadInterfaceLog = async () => {
  rawExportLoading.value = true;
  try {
    await downloadLogFile("raw-access-logs.log", await fetchAllRawAccessLogLines());
  } finally {
    rawExportLoading.value = false;
  }
};

onMounted(async () => {
  await loadRoleTemplates();
  refresh();
  document.addEventListener("visibilitychange", onVisibilityChange);
  startRealtimePolling();
});

onBeforeUnmount(() => {
  document.removeEventListener("visibilitychange", onVisibilityChange);
  stopTimers();
});

defineExpose({ refresh, applyTrendFilter });
</script>

<style scoped>
.access-logs {
  --audit-ink: #1a2332;
  --audit-muted: #64748b;
  --audit-border: #e2e8f0;
  --audit-blue: #3b82f6;
  --audit-cyan: #06b6d4;
  --audit-violet: #8b5cf6;
  --audit-danger: #ef4444;
  --audit-indigo: #6366f1;
  --filter-control-height: 26px;
  --filter-font-size: 12px;
  --card-radius: 12px;
  --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 4px 12px rgba(0, 0, 0, 0.03);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.access-overview {
  padding: 8px 12px 6px;
  border: 1px solid var(--audit-border);
  border-radius: var(--card-radius);
  background: #fff;
  box-shadow: var(--card-shadow);
}

.access-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 32px;
  padding: 0 0 7px;
  border-bottom: 1px solid #edf1f6;
}

.access-toolbar-title h3 {
  margin: 0;
  color: var(--audit-ink);
  font-size: 15px;
  font-weight: 650;
}

.access-toolbar-title p {
  margin: 3px 0 0;
  color: var(--audit-muted);
  font-size: 12px;
}

.access-toolbar-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

.audit-filters {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px 12px;
  background: transparent;
  border: none;
  box-shadow: none;
  border-bottom: 1px solid var(--audit-border);
  margin-bottom: 0;
}

.filter-inputs {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
}

.load-alert {
  flex: 0 0 auto;
}

.filter-time-presets {
  flex-shrink: 0;
}

.filter-time-presets :deep(.el-radio-button__inner) {
  height: var(--filter-control-height);
  padding: 0 12px;
  border-radius: 0;
  font-size: var(--filter-font-size);
  font-weight: 500;
  line-height: calc(var(--filter-control-height) - 2px);
}

.filter-time-presets :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-radius: 7px 0 0 7px;
}

.filter-time-presets :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-radius: 0 7px 7px 0;
}

.filter-time-presets :deep(.el-radio-button.is-active .el-radio-button__inner) {
  font-weight: 600;
}

.filter-role {
  width: 110px;
}

.filter-result {
  width: 88px;
}

.filter-source {
  width: 100px;
}

.filter-keyword {
  flex: 1 1 280px;
  min-width: 240px;
  max-width: 360px;
}

.audit-filters :deep(.el-select__wrapper),
.audit-filters :deep(.el-input__wrapper) {
  min-height: var(--filter-control-height);
  padding-top: 0;
  padding-bottom: 0;
  border-radius: 7px;
  font-size: var(--filter-font-size);
}

.audit-filters :deep(.el-select__placeholder),
.audit-filters :deep(.el-select__selected-item),
.audit-filters :deep(.el-input__inner) {
  font-size: var(--filter-font-size);
  line-height: 1.4;
}

.filter-reset {
  flex-shrink: 0;
  height: var(--filter-control-height);
  padding: 0 11px;
  border-radius: 7px;
  font-size: var(--filter-font-size);
  font-weight: 500;
}

.audit-metrics {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  padding-top: 5px;
}

.metric-card {
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  padding: 5px 12px;
  border-left: 1px solid #edf1f6;
}

.metric-card:first-child {
  border-left: 0;
}

.metric-card::before {
  display: none;
}

.metric-card.blue::before { background: linear-gradient(90deg, #3b82f6, #60a5fa); }
.metric-card.cyan::before { background: linear-gradient(90deg, #06b6d4, #22d3ee); }
.metric-card.violet::before { background: linear-gradient(90deg, #8b5cf6, #a78bfa); }
.metric-card.danger::before { background: linear-gradient(90deg, #ef4444, #f87171); }
.metric-card.indigo::before { background: linear-gradient(90deg, #6366f1, #818cf8); }

.metric-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 8px;
  flex-shrink: 0;
}

.metric-icon svg {
  width: 14px;
  height: 14px;
}

.metric-card.blue .metric-icon { background: #eff6ff; color: #3b82f6; }
.metric-card.cyan .metric-icon { background: #ecfeff; color: #06b6d4; }
.metric-card.violet .metric-icon { background: #f5f3ff; color: #8b5cf6; }
.metric-card.danger .metric-icon { background: #fef2f2; color: #ef4444; }
.metric-card.indigo .metric-icon { background: #eef2ff; color: #6366f1; }

.metric-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.metric-label {
  color: var(--audit-muted);
  font-size: 11px;
  font-weight: 500;
}

.metric-value {
  color: var(--audit-ink);
  font-size: 18px;
  font-weight: 700;
  line-height: 1.1;
}

.access-log-card {
  min-width: 0;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--audit-border);
  border-radius: var(--card-radius);
  background: #fff;
  box-shadow: var(--card-shadow);
}

.section-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 10px;
}

.section-title-group h4 {
  margin: 0;
  color: var(--audit-ink);
  font-size: 15px;
  font-weight: 600;
}

.section-title-group h5 {
  margin: 0;
  color: var(--audit-ink);
  font-size: 13px;
  font-weight: 600;
}

.section-title-group p {
  margin: 4px 0 0;
  color: var(--audit-muted);
  font-size: 12px;
}

.table-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
}



.log-surface {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.table-shell {
  min-width: 0;
  overflow: hidden;
  border: 0;
  border-radius: 0;
}

.access-table {
  --el-table-header-bg-color: #f8fafc;
  font-size: 12px;
}

.access-table :deep(.el-table__cell) {
  padding: 5px 0;
}

.access-table :deep(.el-table__header .cell) {
  color: #64748b;
  font-size: 11.5px;
  font-weight: 600;
}

.access-table :deep(.cell) {
  padding: 0 8px;
}

.access-table :deep(.el-scrollbar__wrap) {
  overflow-x: hidden;
}

.access-table :deep(.el-scrollbar__bar.is-horizontal) {
  display: none;
}

.time-cell,
.ip-cell,
.location-cell,
.elapsed-cell strong {
  color: #475569;
  font-variant-numeric: tabular-nums;
}

.location-cell,
.ip-cell {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-cell,
.request-cell,
.elapsed-cell {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 2px;
}

.account-cell strong,
.request-cell strong {
  overflow: hidden;
  color: var(--audit-ink);
  font-size: 12.5px;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.request-cell strong {
  display: flex;
  align-items: center;
  gap: 6px;
}

.request-cell strong span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-cell span,
.request-cell > span {
  overflow: hidden;
  color: var(--audit-muted);
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.elapsed-cell {
  display: flex;
  align-items: flex-start;
  flex-direction: column;
  gap: 3px;
}

.elapsed-cell strong {
  font-size: 11px;
}

.dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
}

.dialog-head strong {
  color: var(--audit-ink);
  font-size: 17px;
}

.dialog-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

.ip-ranking-dialog {
  display: flex;
  flex-direction: column;
  gap: 12px;
}



.ip-ranking-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  color: var(--audit-muted);
  font-size: 12px;
}

.ip-rank-grid {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px;
}

.ip-rank-card {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  border: 1px solid #edf2f7;
  border-radius: 8px;
  background: #fff;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.ip-rank-card:hover {
  border-color: #dbe5ef;
  box-shadow: 0 6px 14px rgba(15, 23, 42, 0.06);
  transform: translateY(-1px);
}

.risk-rank-card {
  border-color: #dbe5ef;
}

.risk-rank-card--danger {
  border-color: #fecaca;
  background: #fff7f7;
}

.risk-rank-card--warning {
  border-color: #fed7aa;
  background: #fffbeb;
}

.risk-rank-card--info {
  border-color: #dbeafe;
  background: #f8fafc;
}

.rank-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
}

.rank-no {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  background: #f1f5f9;
  color: #94a3b8;
  font-size: 12px;
  font-weight: 700;
}

.rank-no.rank-top {
  background: linear-gradient(135deg, #3b82f6, #6366f1);
  color: #fff;
}

.rank-risk-tag {
  flex-shrink: 0;
}

.rank-card-ip,
.rank-card-meta {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 2px;
}

.rank-card-ip strong {
  overflow: hidden;
  color: var(--audit-ink);
  font-size: 13.5px;
  font-weight: 700;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rank-card-ip span,
.rank-card-meta span,
.rank-card-meta small {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rank-card-ip span {
  color: #94a3b8;
  font-size: 10.5px;
}

.rank-card-meta span {
  color: #475569;
  font-size: 11px;
}

.rank-card-meta small {
  color: var(--audit-muted);
  font-size: 10.5px;
}

.rank-card-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 6px;
}

.rank-card-stats div {
  min-width: 0;
  padding: 6px 7px;
  border-radius: 7px;
  background: #f8fafc;
}

.rank-card-stats span {
  display: block;
  color: var(--audit-muted);
  font-size: 11px;
}

.rank-card-stats strong {
  display: block;
  margin-top: 2px;
  color: var(--audit-ink);
  font-size: 15px;
  line-height: 1.1;
}

.rank-card-stats .denied-highlight {
  color: var(--audit-danger);
}

.terminal-window {
  min-height: 360px;
  max-height: 520px;
  overflow: auto;
  padding: 10px;
  border: 1px solid #1e293b;
  border-radius: 10px;
  background: linear-gradient(180deg, #1e293b, #0f172a);
}

.dialog-terminal-window {
  height: 62vh;
  min-height: 360px;
  max-height: 68vh;
}

.terminal-lines {
  margin: 0;
  color: #e2e8f0;
  font-family: "JetBrains Mono", "SFMono-Regular", "Cascadia Code", "Menlo", monospace;
  font-size: 11.5px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}

.logs-pagination {
  display: flex;
  justify-content: flex-end;
  padding: 8px 12px;
  border-top: 1px solid var(--audit-border);
  background: #fff;
}

.detail-stack {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.detail-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.detail-card,
.detail-section {
  min-width: 0;
  padding: 14px;
  border: 1px solid var(--audit-border);
  border-radius: 12px;
  background: #fff;
}

.detail-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.detail-card span,
.detail-section h4 {
  margin: 0;
  color: var(--audit-muted);
  font-size: 12px;
  font-weight: 600;
}

.detail-card strong {
  color: var(--audit-ink);
  font-size: 15px;
}

.detail-card small,
.detail-section p {
  margin: 0;
  color: #475569;
  font-size: 12px;
  line-height: 1.6;
  word-break: break-word;
}

.detail-section h4 {
  margin-bottom: 8px;
}

.detail-request-line {
  margin-bottom: 10px !important;
  padding: 9px 10px;
  border-radius: 8px;
  background: #f8fafc;
  color: var(--audit-ink) !important;
  font-family: "JetBrains Mono", "SFMono-Regular", "Cascadia Code", "Menlo", monospace;
  font-size: 12px !important;
  font-weight: 600;
}

.detail-field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.detail-field-item {
  min-width: 0;
  padding: 8px 10px;
  border-radius: 8px;
  background: #f8fafc;
}

.detail-field-item span {
  display: block;
  margin-bottom: 3px;
  color: var(--audit-muted);
  font-size: 11px;
  font-weight: 600;
}

.detail-field-item strong {
  display: block;
  overflow-wrap: anywhere;
  color: var(--audit-ink);
  font-size: 12px;
  line-height: 1.5;
}

.detail-raw-line {
  margin: 0;
  overflow: auto;
  padding: 10px;
  border-radius: 8px;
  background: #0f172a;
  color: #e2e8f0;
  font-family: "JetBrains Mono", "SFMono-Regular", "Cascadia Code", "Menlo", monospace;
  font-size: 11.5px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}

.detail-more {
  border-top: 1px solid var(--audit-border);
  border-bottom: 1px solid var(--audit-border);
}

.detail-more :deep(.el-collapse-item__header) {
  height: 42px;
  color: #475569;
  font-size: 12px;
  font-weight: 600;
}

.detail-more :deep(.el-collapse-item__content) {
  padding-bottom: 4px;
}

.detail-subsection {
  padding: 10px 0 14px;
}

.detail-subsection + .detail-subsection {
  border-top: 1px solid #edf2f7;
}

.detail-subsection h5 {
  margin: 0 0 8px;
  color: var(--audit-muted);
  font-size: 11px;
  font-weight: 600;
}

@media (max-width: 1100px) {
  .audit-metrics {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .metric-card:nth-child(odd) {
    border-left: 0;
  }

  .metric-card:nth-child(n + 3) {
    border-top: 1px solid #edf1f6;
  }

  .ip-rank-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (max-width: 920px) {
  .audit-filters {
    flex-wrap: wrap;
  }

  .filter-keyword {
    max-width: none;
  }

  .ip-rank-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (max-width: 720px) {
  .access-toolbar,
  .access-toolbar-actions {
    align-items: flex-start;
    flex-direction: column;
  }

  .access-toolbar-actions {
    width: 100%;
  }

  .audit-metrics {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .ip-rank-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }



  .table-actions {
    justify-content: flex-start;
  }
}

@media (max-width: 520px) {
  .audit-metrics {
    grid-template-columns: 1fr;
  }

  .metric-card {
    border-top: 1px solid #edf1f6;
    border-left: 0;
  }

  .metric-card:first-child {
    border-top: 0;
  }

  .ip-rank-grid {
    grid-template-columns: 1fr;
  }
}
</style>
