/* ============================================================================
 * 서명전에 — 앱인토스 인앱광고 스캐폴드 (Task #117)
 * ----------------------------------------------------------------------------
 * 이 파일의 역할
 *   - TossAdapter.ads.* 위에 얇은 React 레이어를 얹어 스크린이 쉽게 소비하도록.
 *   - 실 SDK (`@apps-in-toss/ads`) 가 붙기 전까지의 "합의된 API 모양" 을 고정.
 *   - 서버 리워드 지급 엔드포인트(`POST /account/grant-ad-reward`) 는 아직
 *     구현되지 않았으므로, 여기서는 그 경로를 "호출 시도" 하고 실패하면 UX
 *     메시지만 띄우는 스캐폴드로 남긴다. 실제 지급은 Task #111 IAP 결제 공사
 *     붙일 때 같은 세션에서 공사한다.
 *
 * 제공하는 것
 *   1) `useBannerAd({ adUnitId })` 훅 — `{ BannerElement, isVisible }`
 *      반환. JSX 최하단에 <BannerElement /> 하나 두면 끝.
 *   2) `runRewardedAd({ adUnitId, accessToken, onReward })` — Promise.
 *      SDK load → show → 'userEarnedReward' → 서버 지급 요청까지 체인.
 *      실패 케이스는 `RewardedAdError` 로 통일된 코드로 throw.
 *
 * 설계 원칙
 *   - ID 가 비어 있거나 SDK 미지원이면 **아무것도 렌더/실행하지 않는다.**
 *     빈 박스·에러 토스트 금지 (앱인토스 정책 · UX 마찰).
 *   - 하루 리워드 상한 프런트 가드 (서버 상한과 이중). 로컬 키는 TossAdapter.storage
 *     로 저장해 앱인토스에서도 persist.
 *   - 모든 외부 효과(SDK 호출, fetch) 는 try/catch 로 감싸 앱이 죽지 않게.
 * ========================================================================== */

const LOCAL_REWARD_KEY = "seomyeongjeone:ads:rewarded-daily";
const LOCAL_REWARD_LIMIT_FALLBACK = 3; // 서버에서 cap 이 내려오기 전 임시 상한.

function _todayStampKST() {
  // KST 기준 하루 경계. (앱인토스는 국내 서비스라 KST 가 사용자가 기대하는 경계.)
  const now = new Date();
  const kstMs = now.getTime() + (9 * 60 - now.getTimezoneOffset()) * 60 * 1000;
  const d = new Date(kstMs);
  return (
    d.getUTCFullYear() * 10000 +
    (d.getUTCMonth() + 1) * 100 +
    d.getUTCDate()
  );
}

async function _readRewardState() {
  try {
    const raw = await TossAdapter.storage.getItem(LOCAL_REWARD_KEY);
    if (!raw) return { stamp: _todayStampKST(), count: 0 };
    const parsed = JSON.parse(String(raw));
    const today = _todayStampKST();
    if (!parsed || parsed.stamp !== today) {
      return { stamp: today, count: 0 };
    }
    return { stamp: today, count: Math.max(0, Number(parsed.count) || 0) };
  } catch (_) {
    return { stamp: _todayStampKST(), count: 0 };
  }
}

async function _bumpRewardState() {
  const cur = await _readRewardState();
  const next = { stamp: cur.stamp, count: cur.count + 1 };
  try {
    await TossAdapter.storage.setItem(LOCAL_REWARD_KEY, JSON.stringify(next));
  } catch (_) { /* storage 고장나도 광고는 돌게 둔다 */ }
  return next;
}

class RewardedAdError extends Error {
  constructor(code, message) {
    super(message || code);
    this.name = "RewardedAdError";
    this.code = code; // "not-supported" | "no-ad-unit" | "cap-reached" | "sdk-load" | "sdk-show" | "reward-grant"
  }
}

/** 배너 훅. JSX 예:
 *     const { BannerElement } = useBannerAd({ adUnitId: auth.adsBannerUnitId });
 *     return <div>...<BannerElement /></div>;
 *
 * ID 가 비어 있으면 BannerElement 는 `null` 을 렌더. 호출측에서 조건부로
 * 감쌀 필요 없음. useMemo 로 banner 인스턴스를 캐시해 rerender 시 재생성
 * 하지 않는다 (TossBanner 내부 SDK 상태 초기화 비용 회피).
 */
function useBannerAd({ adUnitId } = {}) {
  const banner = React.useMemo(() => {
    if (!adUnitId) return null;
    try {
      return TossAdapter.ads.createBanner({ adUnitId });
    } catch (err) {
      console.warn("[ads] createBanner failed:", err);
      return null;
    }
  }, [adUnitId]);

  const isVisible = !!(banner && banner.isSupported);
  const BannerElement = isVisible ? banner.Element : () => null;
  return { BannerElement, isVisible };
}

/** 보상형 광고를 "한 번 돌려서 리워드 받기" 까지의 전체 플로우.
 *
 * @param {Object} opts
 * @param {string} opts.adUnitId                       — 광고 유닛 ID (필수).
 * @param {string} [opts.accessToken]                  — Supabase access_token. 서버 지급 시 Authorization 헤더에 실음.
 * @param {number} [opts.grantUnits]                   — 1회 지급 무료권 수. /client-config 의 ads_rewarded_grant_units.
 * @param {number} [opts.dailyCap]                     — 프런트 상한 (서버 상한과 이중 방어).
 * @param {(n: number) => void} [opts.onReward]        — 리워드 성공 시 호출. 지급된 무료권 수 전달.
 * @returns {Promise<{ granted: number }>}             — 성공 시 지급 단위 수 반환.
 * @throws  {RewardedAdError}                          — 위에 기술한 code 로 실패 원인 식별.
 */
async function runRewardedAd({
  adUnitId,
  accessToken,
  grantUnits = 1,
  dailyCap = LOCAL_REWARD_LIMIT_FALLBACK,
  onReward,
} = {}) {
  if (!adUnitId) throw new RewardedAdError("no-ad-unit", "광고 유닛이 설정되지 않았어요.");
  if (!TossAdapter.ads.isRewardedSupported()) {
    throw new RewardedAdError("not-supported", "지금은 광고가 준비되지 않았어요.");
  }

  const cur = await _readRewardState();
  if (cur.count >= Math.max(1, dailyCap)) {
    throw new RewardedAdError("cap-reached", "오늘 받을 수 있는 광고 보상을 다 받았어요. 내일 다시 시도해 주세요.");
  }

  try {
    await TossAdapter.ads.loadRewarded({ adUnitId });
  } catch (err) {
    if (err?.name === "TossNotSupportedError") {
      throw new RewardedAdError("not-supported", "지금은 광고가 준비되지 않았어요.");
    }
    throw new RewardedAdError("sdk-load", err?.message || "광고를 불러오지 못했어요.");
  }

  // 보상형은 'userEarnedReward' 이벤트 수신이 성공 조건. Promise 로 감싸
  // 이벤트 완료 or 실패 어느 쪽으로든 resolve/reject 되게 한다.
  const earned = await new Promise((resolve, reject) => {
    TossAdapter.ads
      .showRewarded({
        adUnitId,
        onEvent: (ev) => {
          if (!ev || typeof ev !== "object") return;
          if (ev.type === "userEarnedReward") resolve(true);
          else if (ev.type === "closed") resolve(false); // 보상 없이 닫힘
          else if (ev.type === "error") reject(new RewardedAdError("sdk-show", ev.error || "광고 재생에 실패했어요."));
        },
      })
      .catch((err) => {
        if (err?.name === "TossNotSupportedError") {
          reject(new RewardedAdError("not-supported", "지금은 광고가 준비되지 않았어요."));
        } else {
          reject(new RewardedAdError("sdk-show", err?.message || "광고 재생에 실패했어요."));
        }
      });
  });

  if (!earned) {
    // 사용자가 끝까지 안 보고 닫은 경우 — 에러는 아니지만 지급도 없다.
    return { granted: 0 };
  }

  // 서버에 리워드 지급 요청. 엔드포인트가 아직 없으면 404 → 그 경우엔 프런트
  // 로컬 카운트만 올리고 "준비 중" 메시지를 남긴다. (SDK 가 이미 다 돌렸는데
  // 서버가 죽으면 유저 경험상 최악이라 로컬에선 부드럽게 fail soft.)
  if (accessToken) {
    try {
      const res = await fetch("/account/grant-ad-reward", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${accessToken}`,
        },
        body: JSON.stringify({ ad_unit_id: adUnitId, grant_units: grantUnits }),
      });
      if (!res.ok) {
        // 엔드포인트 미구현(404) 또는 일시적 에러.
        // 광고는 본 상태이므로 local count 만 올리고 호출측에 경고만 남긴다.
        console.warn("[ads] grant-ad-reward failed:", res.status);
        throw new RewardedAdError(
          "reward-grant",
          res.status === 404
            ? "지금은 광고 보상이 준비 중이에요. 잠시 후 다시 시도해 주세요."
            : "보상을 지급하지 못했어요. 잠시 후 다시 시도해 주세요."
        );
      }
    } catch (err) {
      if (err instanceof RewardedAdError) throw err;
      throw new RewardedAdError("reward-grant", err?.message || "보상 서버에 연결하지 못했어요.");
    }
  }

  await _bumpRewardState();
  if (typeof onReward === "function") {
    try { onReward(grantUnits); } catch (_) { /* noop */ }
  }
  return { granted: grantUnits };
}

/** 오늘 남은 리워드 횟수 (UI 에 "X회 남음" 표시용). */
async function getRewardedQuotaToday({ dailyCap = LOCAL_REWARD_LIMIT_FALLBACK } = {}) {
  const cur = await _readRewardState();
  const cap = Math.max(1, dailyCap);
  return { used: cur.count, cap, remaining: Math.max(0, cap - cur.count) };
}

// 전역 노출 — Babel(dev) 경로에서 import 없이 바로 쓸 수 있게.
window.SeomyeongAds = {
  useBannerAd,
  runRewardedAd,
  getRewardedQuotaToday,
  RewardedAdError,
};
