const TEXT_ENCODER = new TextEncoder();

export const DOWNLOAD_TARGET = "/downloads/Sensa_basic_v1.0.1.zip";

export async function trackSiteEvent(env, request, eventType, details = {}) {
  const kv = env?.SITE_STATS;
  if (!kv) {
    return { ok: false, reason: "missing_binding" };
  }

  const dateKey = getDateKey();
  const visitorKey = await getVisitorKey(request);
  const pagePath = sanitizePath(details.pagePath || request?.url);
  const geo = extractGeoInfo(request);

  await incrementCounter(kv, buildCounterKey("total", eventType));
  await incrementCounter(kv, buildCounterKey(`day:${dateKey}`, eventType));
  await incrementCounter(kv, buildPathCounterKey("total", eventType, pagePath));
  await incrementCounter(kv, buildPathCounterKey(`day:${dateKey}`, eventType, pagePath));
  await incrementGeoCounters(kv, dateKey, eventType, geo);

  if (eventType === "page_view") {
    const isUniqueToday = await markUnique(kv, `visitor:day:${dateKey}`, visitorKey, 60 * 60 * 24 * 3);
    if (isUniqueToday) {
      await incrementCounter(kv, buildCounterKey(`day:${dateKey}`, "unique_visitors"));
    }

    const isUniqueAllTime = await markUnique(kv, "visitor:all", visitorKey);
    if (isUniqueAllTime) {
      await incrementCounter(kv, buildCounterKey("total", "unique_visitors"));
    }
  }

  if (eventType === "download_click") {
    const isUniqueToday = await markUnique(kv, `download:day:${dateKey}`, visitorKey, 60 * 60 * 24 * 3);
    if (isUniqueToday) {
      await incrementCounter(kv, buildCounterKey(`day:${dateKey}`, "unique_downloaders"));
    }

    const isUniqueAllTime = await markUnique(kv, "download:all", visitorKey);
    if (isUniqueAllTime) {
      await incrementCounter(kv, buildCounterKey("total", "unique_downloaders"));
    }
  }

  return { ok: true };
}

export async function readSiteStats(env) {
  const kv = env?.SITE_STATS;
  if (!kv) {
    return null;
  }

  const dateKey = getDateKey();
  const [pageViews, uniqueVisitors, downloadClicks, uniqueDownloaders] = await Promise.all([
    readCounter(kv, buildCounterKey("total", "page_view")),
    readCounter(kv, buildCounterKey("total", "unique_visitors")),
    readCounter(kv, buildCounterKey("total", "download_click")),
    readCounter(kv, buildCounterKey("total", "unique_downloaders"))
  ]);

  const [todayPageViews, todayUniqueVisitors, todayDownloadClicks, todayUniqueDownloaders] = await Promise.all([
    readCounter(kv, buildCounterKey(`day:${dateKey}`, "page_view")),
    readCounter(kv, buildCounterKey(`day:${dateKey}`, "unique_visitors")),
    readCounter(kv, buildCounterKey(`day:${dateKey}`, "download_click")),
    readCounter(kv, buildCounterKey(`day:${dateKey}`, "unique_downloaders"))
  ]);

  const daily = await readDailySeries(kv, 30);
  const [countryPageViewsToday, countryDownloadsToday, chinaRegionsToday] = await Promise.all([
    readGeoBreakdown(kv, `stats:geo:day:${dateKey}:page_view:country:`),
    readGeoBreakdown(kv, `stats:geo:day:${dateKey}:download_click:country:`),
    readChinaRegionBreakdown(kv, dateKey)
  ]);

  return {
    generatedAt: new Date().toISOString(),
    note: "UV 为基于浏览器和 IP 指纹的近似值，适合观察趋势，不等同于严格实名去重。",
    totals: {
      pageViews,
      uniqueVisitors,
      downloadClicks,
      uniqueDownloaders
    },
    today: {
      date: dateKey,
      pageViews: todayPageViews,
      uniqueVisitors: todayUniqueVisitors,
      downloadClicks: todayDownloadClicks,
      uniqueDownloaders: todayUniqueDownloaders
    },
    daily,
    geo: {
      countryPageViewsToday,
      countryDownloadsToday,
      chinaRegionsToday
    }
  };
}

async function incrementCounter(kv, key, amount = 1) {
  const current = Number.parseInt((await kv.get(key)) || "0", 10) || 0;
  const nextValue = current + amount;
  await kv.put(key, String(nextValue));
  return nextValue;
}

async function readCounter(kv, key) {
  return Number.parseInt((await kv.get(key)) || "0", 10) || 0;
}

async function incrementGeoCounters(kv, dateKey, eventType, geo) {
  const country = normalizeCountryCode(geo.country);
  const regionCode = normalizeRegionCode(geo.regionCode);

  await incrementCounter(kv, buildGeoKey(`day:${dateKey}`, eventType, "country", country));
  await incrementCounter(kv, buildGeoKey("total", eventType, "country", country));

  if (country === "CN" && regionCode) {
    await incrementCounter(kv, buildGeoKey(`day:${dateKey}`, eventType, "region", regionCode));
    await incrementCounter(kv, buildGeoKey("total", eventType, "region", regionCode));
  }
}

async function readGeoBreakdown(kv, prefix) {
  const items = await listCounterEntries(kv, prefix);
  return items
    .map(({ key, value }) => ({
      code: key.slice(prefix.length),
      label: getCountryLabel(key.slice(prefix.length)),
      count: value
    }))
    .filter((item) => item.count > 0)
    .sort((left, right) => right.count - left.count || left.label.localeCompare(right.label));
}

async function readChinaRegionBreakdown(kv, dateKey) {
  const [pageViews, downloads] = await Promise.all([
    listCounterEntries(kv, `stats:geo:day:${dateKey}:page_view:region:`),
    listCounterEntries(kv, `stats:geo:day:${dateKey}:download_click:region:`)
  ]);

  const byRegion = new Map();

  for (const item of pageViews) {
    const code = item.key.split(":").at(-1) || "";
    const entry = byRegion.get(code) || createEmptyRegionEntry(code);
    entry.pageViews = item.value;
    byRegion.set(code, entry);
  }

  for (const item of downloads) {
    const code = item.key.split(":").at(-1) || "";
    const entry = byRegion.get(code) || createEmptyRegionEntry(code);
    entry.downloadClicks = item.value;
    byRegion.set(code, entry);
  }

  return [...byRegion.values()]
    .filter((item) => item.pageViews > 0 || item.downloadClicks > 0)
    .sort((left, right) => {
      const rightScore = right.pageViews + right.downloadClicks;
      const leftScore = left.pageViews + left.downloadClicks;
      return rightScore - leftScore || left.label.localeCompare(right.label);
    });
}

async function listCounterEntries(kv, prefix) {
  const results = [];
  let cursor = undefined;

  do {
    const page = await kv.list({ prefix, cursor });
    for (const keyInfo of page.keys || []) {
      const value = await readCounter(kv, keyInfo.name);
      results.push({ key: keyInfo.name, value });
    }
    cursor = page.list_complete ? undefined : page.cursor;
  } while (cursor);

  return results;
}

async function markUnique(kv, scope, visitorKey, expirationTtl) {
  const key = `stats:seen:${scope}:${visitorKey}`;
  const existing = await kv.get(key);
  if (existing) {
    return false;
  }

  const options = expirationTtl ? { expirationTtl } : undefined;
  await kv.put(key, "1", options);
  return true;
}

function buildCounterKey(scope, metric) {
  return `stats:counter:${scope}:${metric}`;
}

function buildPathCounterKey(scope, metric, pagePath) {
  return `stats:path:${scope}:${metric}:${pagePath || "/"}`;
}

function buildGeoKey(scope, metric, dimension, value) {
  return `stats:geo:${scope}:${metric}:${dimension}:${value}`;
}

function sanitizePath(input) {
  try {
    const url = new URL(input, "https://sensa.pages.dev");
    return url.pathname || "/";
  } catch (error) {
    if (typeof input === "string" && input.startsWith("/")) {
      return input;
    }
    return "/";
  }
}

function getDateKey() {
  return new Date().toISOString().slice(0, 10);
}

async function readDailySeries(kv, days) {
  const dateKeys = buildRecentDateKeys(days);
  const series = await Promise.all(
    dateKeys.map(async (date) => {
      const [pageViews, uniqueVisitors, downloadClicks, uniqueDownloaders] = await Promise.all([
        readCounter(kv, buildCounterKey(`day:${date}`, "page_view")),
        readCounter(kv, buildCounterKey(`day:${date}`, "unique_visitors")),
        readCounter(kv, buildCounterKey(`day:${date}`, "download_click")),
        readCounter(kv, buildCounterKey(`day:${date}`, "unique_downloaders"))
      ]);

      return {
        date,
        pageViews,
        uniqueVisitors,
        downloadClicks,
        uniqueDownloaders
      };
    })
  );

  return series;
}

function buildRecentDateKeys(days) {
  const values = [];
  const now = new Date();
  for (let index = days - 1; index >= 0; index -= 1) {
    const date = new Date(now);
    date.setUTCDate(date.getUTCDate() - index);
    values.push(date.toISOString().slice(0, 10));
  }
  return values;
}

async function getVisitorKey(request) {
  const ip = request.headers.get("CF-Connecting-IP") || "unknown-ip";
  const ua = request.headers.get("User-Agent") || "unknown-ua";
  const language = request.headers.get("Accept-Language") || "unknown-lang";
  return hashString(`${ip}|${ua}|${language}`);
}

function extractGeoInfo(request) {
  const cf = request.cf || {};
  const country = cf.country || request.headers.get("CF-IPCountry") || "UNKNOWN";
  const regionCode = cf.regionCode || "";

  return {
    country,
    regionCode
  };
}

function normalizeCountryCode(value) {
  const code = String(value || "").trim().toUpperCase();
  return code || "UNKNOWN";
}

function normalizeRegionCode(value) {
  return String(value || "").trim().toUpperCase();
}

function createEmptyRegionEntry(code) {
  return {
    code,
    label: getChinaRegionLabel(code),
    pageViews: 0,
    downloadClicks: 0
  };
}

function getCountryLabel(code) {
  return COUNTRY_LABELS[code] || code || "UNKNOWN";
}

function getChinaRegionLabel(code) {
  return CHINA_REGION_LABELS[code] || code || "UNKNOWN";
}

const COUNTRY_LABELS = {
  CN: "中国",
  HK: "中国香港",
  MO: "中国澳门",
  TW: "中国台湾",
  US: "美国",
  JP: "日本",
  SG: "新加坡",
  KR: "韩国",
  DE: "德国",
  GB: "英国",
  FR: "法国",
  CA: "加拿大",
  AU: "澳大利亚",
  UNKNOWN: "未知"
};

const CHINA_REGION_LABELS = {
  AH: "安徽",
  BJ: "北京",
  CQ: "重庆",
  FJ: "福建",
  GD: "广东",
  GS: "甘肃",
  GX: "广西",
  GZ: "贵州",
  HA: "河南",
  HB: "湖北",
  HE: "河北",
  HI: "海南",
  HK: "香港",
  HL: "黑龙江",
  HN: "湖南",
  JL: "吉林",
  JS: "江苏",
  JX: "江西",
  LN: "辽宁",
  MO: "澳门",
  NM: "内蒙古",
  NX: "宁夏",
  QH: "青海",
  SC: "四川",
  SD: "山东",
  SH: "上海",
  SN: "陕西",
  SX: "山西",
  TJ: "天津",
  TW: "台湾",
  XJ: "新疆",
  XZ: "西藏",
  YN: "云南",
  ZJ: "浙江"
};

async function hashString(value) {
  const digest = await crypto.subtle.digest("SHA-256", TEXT_ENCODER.encode(value));
  return Array.from(new Uint8Array(digest), (item) => item.toString(16).padStart(2, "0")).join("");
}
