import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PrismaService } from "../prisma/prisma.service";

/**
 * Reader/writer for `app_settings` — the configuration staff can change
 * without a redeploy.
 *
 * Every setting resolves through the same three-step ladder: the database row
 * if an operator has set one, else the environment variable it used to live
 * in, else a hardcoded default. That ordering is what makes this migration
 * safe — an existing deployment keeps behaving exactly as it did until
 * somebody edits the value in the UI, and `source` tells the UI which of the
 * three it is looking at so "this came from the env, editing it here will
 * take over" is visible rather than surprising.
 */

export const SETTING_KEYS = {
  /** Comma-separated recipients of the per-job notification summary. */
  notificationAdminEmails: "notification.adminEmails",
  /** JSON cadence of the automatic servicios sweep. */
  scheduleServicios: "notification.schedule.servicios",
  /** JSON cadence of the automatic pólizas renewal sweep. */
  schedulePolizas: "notification.schedule.polizas",
} as const;

/** Where a resolved value came from. Shown in the UI. */
export type SettingSource = "db" | "env" | "default";

export interface ResolvedSetting<T> {
  value: T;
  source: SettingSource;
  updatedAt: Date | null;
  updatedById: string | null;
}

/** Last resort when neither the database nor the environment says otherwise.
 *  Matches what `NotificationsService` hardcoded before this table existed. */
const DEFAULT_ADMIN_EMAILS = ["rmancinas@freakma.net", "mpulido@freakma.net"];

/** Deliberately permissive — this rejects "not an address at all", not
 *  "not deliverable". Only SES can tell us the latter, and a validator strict
 *  enough to argue with is a validator that blocks a legitimate address. */
const EMAIL_RE = /^[^\s@,]+@[^\s@,]+\.[^\s@,]+$/;

export function parseEmailList(raw: string): string[] {
  return raw
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);
}

export function invalidEmails(list: string[]): string[] {
  return list.filter((e) => !EMAIL_RE.test(e));
}

@Injectable()
export class SettingsService {
  private readonly logger = new Logger(SettingsService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly config: ConfigService,
  ) {}

  /**
   * Recipients of the per-job summary email.
   *
   * Read on every send rather than cached at boot: the point of moving this
   * out of the environment was that it changes while the app is running, and
   * a cache would reintroduce exactly the restart-to-apply behaviour we are
   * removing. It is one indexed primary-key lookup per sweep, not per email.
   */
  async notificationAdminEmails(): Promise<ResolvedSetting<string[]>> {
    const row = await this.read(SETTING_KEYS.notificationAdminEmails);
    if (row) {
      const parsed = parseEmailList(row.value);
      // An empty stored value is a legitimate choice — "send no summaries" —
      // and must not silently fall through to the env or the defaults, or an
      // operator who cleared the field would keep receiving mail.
      return {
        value: parsed,
        source: "db",
        updatedAt: row.updatedAt,
        updatedById: row.updatedById,
      };
    }

    const env = this.config.get<string>("NOTIFICATION_ADMIN_EMAILS");
    if (env && env.trim()) {
      return {
        value: parseEmailList(env),
        source: "env",
        updatedAt: null,
        updatedById: null,
      };
    }

    return {
      value: [...DEFAULT_ADMIN_EMAILS],
      source: "default",
      updatedAt: null,
      updatedById: null,
    };
  }

  /** Persist the summary recipients. An empty list is stored as an empty
   *  string and means "nobody" — see the read path above. */
  async setNotificationAdminEmails(
    emails: string[],
    userId: string,
  ): Promise<ResolvedSetting<string[]>> {
    await this.write(
      SETTING_KEYS.notificationAdminEmails,
      emails.join(","),
      userId,
    );
    return this.notificationAdminEmails();
  }

  /**
   * Cadence of one automatic envío, stored as JSON.
   *
   * No env rung on this ladder: a schedule was never an environment variable
   * (it was a `@Cron` literal in the source), so the only two sources are the
   * operator's row and the caller's default — which is the previous hardcoded
   * behaviour. A row that fails to parse is treated as absent and logged
   * rather than thrown: a bad JSON blob must not take the scheduler down with
   * it, and falling back to the shipped cadence is the safe reading.
   */
  async notificationSchedule<T>(
    kind: "servicios" | "polizas",
    fallback: T,
  ): Promise<ResolvedSetting<T>> {
    const key =
      kind === "servicios"
        ? SETTING_KEYS.scheduleServicios
        : SETTING_KEYS.schedulePolizas;
    const row = await this.read(key);
    if (row) {
      try {
        return {
          value: { ...fallback, ...(JSON.parse(row.value) as T) },
          source: "db",
          updatedAt: row.updatedAt,
          updatedById: row.updatedById,
        };
      } catch (error) {
        this.logger.warn(
          `Setting ${key} is not valid JSON, using the default: ` +
            `${(error as Error).message}`,
        );
      }
    }
    return { value: fallback, source: "default", updatedAt: null, updatedById: null };
  }

  async setNotificationSchedule(
    kind: "servicios" | "polizas",
    schedule: unknown,
    userId: string,
  ): Promise<void> {
    await this.write(
      kind === "servicios"
        ? SETTING_KEYS.scheduleServicios
        : SETTING_KEYS.schedulePolizas,
      JSON.stringify(schedule),
      userId,
    );
  }

  private read(key: string) {
    return this.prisma.appSetting.findUnique({ where: { key } });
  }

  private async write(key: string, value: string, userId: string) {
    await this.prisma.appSetting.upsert({
      where: { key },
      create: { key, value, updatedById: userId },
      update: { value, updatedById: userId },
    });
  }
}
