/**
 * アプリ共通ロガー。出力は stdout のみ（ファイル化・ローテーションは OS 側）。
 *
 * mixin() で request_id / data_source を自動付与する理由:
 * 呼び出し忘れで相関が切れるのを防ぎ、USE_FIXTURES の解釈を lib/db の export に一本化するため。
 */
import pino from "pino";
import { USE_FIXTURES } from "@/lib/db/use-fixtures";
import { getRequestId } from "@/lib/logger/context";
import type {
  ForbiddenFieldsStrippedLogEvent,
  LogEvent,
} from "@/lib/logger/events";
import {
  enforceForbiddenErrorFieldsPolicy,
  enforceForbiddenFieldsPolicy,
} from "@/lib/logger/redact";
import { formatLogTimestamp } from "@/lib/logger/timestamp";

export type LogLevel = "info" | "warn" | "error";

function resolveLevel(): string {
  if (process.env.LOG_LEVEL) return process.env.LOG_LEVEL;
  return isProduction() ? "info" : "debug";
}

function isProduction(): boolean {
  return process.env.NODE_ENV === "production";
}

/**
 * pino インスタンスを組み立てる。
 * 出力先を差し替えられるようにしてあるのは、スモークテストが本番と
 * 同一設定の出力を検証できるようにするため（設定を二重に書かない）。
 */
export function createLogger(destination?: pino.DestinationStream): pino.Logger {
  const options: pino.LoggerOptions = {
    level: resolveLevel(),
    timestamp: () => `,"timestamp":${JSON.stringify(formatLogTimestamp())}`,
    formatters: {
      /** 数値レベルだと grep しづらいので info/warn/error の文字列にする */
      level(label) {
        return { level: label };
      },
    },
    mixin() {
      return {
        request_id: getRequestId() ?? "-",
        data_source: USE_FIXTURES ? "fixtures" : "prisma",
      };
    },
  };
  return destination ? pino(options, destination) : pino(options);
}

export const logger = createLogger();

/**
 * 指定インスタンスへ1行出す実体。
 *
 * 禁止フィールドの扱いを環境で分けるのは、ログ呼び出しが catch 節の内側から
 * 行われるため。本番で例外を投げると元のエラー処理を壊し、ログの機構が
 * リクエストを 500 にしてしまう。「ログの失敗でアプリを落とさない」を優先する。
 *
 * 本番で検出した場合、元のログ（禁止キーを除去したもの・元のレベルのまま）とは
 * **別に** warn を1行出す。docs/logging-design.md §10 は「該当キーを取り除いて warn で
 * 記録し、ログ呼び出し自体は成功させる」としか定めておらず、**別行にするのは実装側の判断**。
 * 分ける理由: 元のログを元のレベルのまま残しつつ、除去が起きた事実を warn で可視化するため。
 * 元のレベルを warn へ格上げして1行にまとめる方式だと、元が error のときに
 * 「禁止フィールドが渡された」事実が warn として残らないため、常に分離する。
 * warn 側には値を一切載せず、キー名（forbidden_keys）だけを記録する。
 */
export function emitTo(
  target: pino.Logger,
  level: LogLevel,
  payload: LogEvent,
  err?: unknown
): void {
  const production = isProduction();
  const { payload: safe, leaked } = enforceForbiddenFieldsPolicy(
    payload,
    production
  );

  // err は error レベルのみ出力する。出さない場合は検査もしない（漏れる経路が無い）
  const emitsErr = level === "error" && err !== undefined;
  const errResult = emitsErr
    ? enforceForbiddenErrorFieldsPolicy(err, production)
    : { err, leaked: [] as string[] };

  // 検査を先に済ませてから書く。非本番の例外は1行も出さずに投げる
  const allLeaked = [...new Set([...leaked, ...errResult.leaked])];
  if (allLeaked.length > 0) {
    const strippedEvent: ForbiddenFieldsStrippedLogEvent = {
      event: "log.forbidden_fields_stripped",
      forbidden_keys: allLeaked,
    };
    target.warn(strippedEvent);
  }

  // message / stack に施設名等が混ざりうるため info / warn には err を付けない
  if (emitsErr) {
    target.error({ ...safe, err: errResult.err });
    return;
  }
  target[level](safe);
}

/**
 * allowlist 型のイベントを1行 JSON で出す。
 * Error は pino 標準の err シリアライザに渡し、JSON.stringify(error) は使わない。
 */
export function logEvent(
  level: "error",
  payload: LogEvent,
  err?: unknown
): void;
export function logEvent(level: "info" | "warn", payload: LogEvent): void;
export function logEvent(
  level: LogLevel,
  payload: LogEvent,
  err?: unknown
): void {
  emitTo(logger, level, payload, err);
}

export {
  runWithRequestId,
  getRequestId,
  withRequestId,
  normalizeRequestId,
  REQUEST_ID_HEADER,
} from "@/lib/logger/context";
export type { LogEvent } from "@/lib/logger/events";
