// ポイント & 抽選の中核ロジック（純粋関数、テスト対象）
// エキスパの「クリックブースター / くじ引き機能」を参考にした仕様:
// - 本文URLクリック / メール開封 / 新規登録 でポイント付与
//   （開封・クリックとも「配信1通につき初回のみ」。付与対象は自分宛に実際に送られた配信に限る）
// - 貯まったポイントを消費してくじを引く（景品ごとに当選確率% と 当選本数上限）
// - 確率合計の残りは「はずれ」。当選本数上限に達した景品は当たらない
import type { LotteryPrize, PointRules } from "./types.js";

export const DEFAULT_POINT_RULES: PointRules = {
  openPoints: 10,
  clickPoints: 20,
  signupPoints: 50,
};

/** settings の生値から PointRules を復元（不正値は既定値へ） */
export function parsePointRules(raw: string | null | undefined): PointRules {
  if (!raw) return { ...DEFAULT_POINT_RULES };
  try {
    const p = JSON.parse(raw) as Partial<PointRules>;
    const n = (v: unknown, d: number) =>
      typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : d;
    return {
      openPoints: n(p.openPoints, DEFAULT_POINT_RULES.openPoints),
      clickPoints: n(p.clickPoints, DEFAULT_POINT_RULES.clickPoints),
      signupPoints: n(p.signupPoints, DEFAULT_POINT_RULES.signupPoints),
    };
  } catch {
    return { ...DEFAULT_POINT_RULES };
  }
}

/**
 * 抽選1回分の判定。
 * @param prizes 景品一覧（active のみ渡すこと）
 * @param winCounts 景品IDごとの既当選数（max_winners 判定用）
 * @param rand [0,1) の乱数
 * @returns 当選した景品、または null（はずれ）
 *
 * 仕様: 各景品の probability(%) を [0,100) 上に並べ、rand*100 が区間内に
 * 落ちたら当選。上限到達済みの景品は区間から除外（=その分はずれが増える）。
 * エキスパ同様「当選本数を設定すると、それ以上の当選がされなくなります」。
 */
export function drawPrize(
  prizes: LotteryPrize[],
  winCounts: Map<number, number>,
  rand: number,
): LotteryPrize | null {
  const roll = rand * 100;
  let cursor = 0;
  for (const p of prizes) {
    if (!p.active || p.probability <= 0) continue;
    const won = winCounts.get(p.id) ?? 0;
    if (p.max_winners > 0 && won >= p.max_winners) continue; // 在庫切れ → はずれ枠へ
    const upper = cursor + p.probability;
    if (roll >= cursor && roll < upper) return p;
    cursor = upper;
  }
  return null;
}

/** 有効な確率合計（%）。100 を超える設定は登録時に弾くための検証にも使う */
export function totalProbability(prizes: Pick<LotteryPrize, "probability" | "active">[]): number {
  return prizes.filter((p) => p.active).reduce((a, p) => a + Math.max(0, p.probability), 0);
}

// ---------- メールへのトラッキング組み込み ----------

/** 開封計測ピクセルの <img> タグ */
export function openPixelTag(baseUrl: string, scope: "bc" | "step", refId: number, token: string): string {
  const base = baseUrl.replace(/\/$/, "");
  return `<img src="${base}/t/o/${scope}/${refId}/${token}" width="1" height="1" alt="" style="display:none">`;
}

/**
 * 本文内の http(s) リンクを計測URLへ書き換える。
 * resolve は (元URL) => 計測リンクID を返す非同期関数（Store 経由で採番）。
 * 書き換え対象: <a href="http..."> のみ。unsub(/u/)・既に計測URL(/t/c/) は除外。
 */
export async function rewriteLinks(
  html: string,
  baseUrl: string,
  token: string,
  resolve: (url: string) => Promise<number>,
): Promise<string> {
  const base = baseUrl.replace(/\/$/, "");
  const re = /(<a\b[^>]*\bhref=")(https?:\/\/[^"]+)(")/gi;
  const matches = [...html.matchAll(re)];
  let out = html;
  for (const m of matches) {
    const url = m[2];
    if (url.startsWith(`${base}/u/`) || url.includes("/t/c/")) continue;
    const linkId = await resolve(url);
    // 置換値を関数形で渡し、本文由来の $& / $1 等が特殊シーケンスとして展開されるのを防ぐ
    const replacement = `${m[1]}${base}/t/c/${linkId}/${token}${m[3]}`;
    out = out.replace(m[0], () => replacement);
  }
  return out;
}

/** マイページ（ポイント確認・抽選会）への案内フッター */
export function portalFooter(baseUrl: string, token: string): string {
  const url = `${baseUrl.replace(/\/$/, "")}/p/${token}`;
  return `
<div style="font-size:12px;color:#888;line-height:1.6;margin-top:8px">
  🎁 貯まったポイントの確認・抽選会への参加は <a href="${url}">マイページ</a> から。
</div>`;
}
