import { eq } from 'drizzle-orm';
import { jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import type { Querier } from '@platform-modules/db';
import type { MaintenanceConfig } from '@platform-modules/util/maintenance';
import type { ConsentCategory } from '@platform-modules/content/privacy';

export class SettingsValidationError extends Error {
  readonly name = 'SettingsValidationError';

  constructor(
    readonly field: string,
    readonly detail: string,
  ) {
    super(`settings validation failed: ${field} — ${detail}`);
  }
}

export const siteSettings = pgTable('site_settings', {
  key: text('key').primaryKey(),
  value: jsonb('value').notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true, precision: 3 }).notNull().defaultNow(),
});

export const settingsSchema = { siteSettings };
export type SettingsSchema = typeof settingsSchema;

export async function getAllSettings(db: Querier<SettingsSchema>): Promise<Record<string, unknown>> {
  const rows = await db.select().from(siteSettings);
  return Object.fromEntries(rows.map((row) => [row.key, row.value]));
}

export async function getSetting(
  db: Querier<SettingsSchema>,
  key: string,
): Promise<unknown | null> {
  const [row] = await db.select().from(siteSettings).where(eq(siteSettings.key, key)).limit(1);
  return row?.value ?? null;
}

export async function setSetting(
  db: Querier<SettingsSchema>,
  key: string,
  value: unknown,
): Promise<void> {
  if (typeof key !== 'string' || !key.trim()) {
    throw new SettingsValidationError('key', 'must be a non-empty string');
  }
  if (value === undefined) {
    throw new SettingsValidationError('value', 'must be defined');
  }

  const now = new Date();
  await db
    .insert(siteSettings)
    .values({ key, value, updatedAt: now })
    .onConflictDoUpdate({
      target: siteSettings.key,
      set: { value, updatedAt: now },
    });
}

export const SETTINGS_KEYS = {
  theme: 'theme',
  homepage: 'homepage',
  maintenance: 'maintenance',
  siteName: 'site_name',
  siteConfigured: 'site_configured',
  siteLogoUrl: 'site_logo_url',
  consent: 'consent',
  privacyInput: 'privacy_input',
  hideModeToggle: 'hide_mode_toggle',
  setupMethod: 'setup_method',
  discussion: 'discussion',
} as const;

export type HomepageConfig = { mode: 'feed' } | { mode: 'page'; slug: string };

const SLUG_MAX = 200;

/**
 * Parse the stored `homepage` setting into a typed config. ALWAYS resolves to a valid config —
 * any malformed/absent value falls back to `{ mode: 'feed' }` so `/` never breaks on bad settings.
 */
export function parseHomepage(value: unknown): HomepageConfig {
  if (value && typeof value === 'object') {
    const v = value as { mode?: unknown; slug?: unknown };
    if (v.mode === 'page' && typeof v.slug === 'string') {
      const slug = v.slug.trim();
      if (slug.length > 0 && slug.length <= SLUG_MAX) return { mode: 'page', slug };
    }
  }
  return { mode: 'feed' };
}

/**
 * Parse the stored `hide_mode_toggle` setting. ALWAYS resolves to a boolean; **fail-closed to `false`
 * (toggle SHOWN)** so visitors keep light/dark control unless the admin EXPLICITLY hides it. Requires
 * a STRICT boolean `true` — truthy junk ('true', 1, {}) never hides the control.
 */
export function parseHideModeToggle(value: unknown): boolean {
  return value === true;
}

export const DEFAULT_SITE_NAME = 'mod-cms';
const SITE_NAME_MAX = 100;
const LOGO_URL_MAX = 2048;

/**
 * True iff `value` is an absolute https:// URL within the length cap. The single source of truth for
 * logo-URL acceptance — used both client-side (inline UX validation) and at render (fail-closed parse).
 * https-only: blocks javascript:/data: scheme-confusion and mixed-content; relative/host-relative
 * rejected (the logo renders cross-origin in an <img src>, so it must be a fully-qualified https URL).
 */
export function isValidLogoUrl(value: unknown): boolean {
  if (typeof value !== 'string') return false;
  const trimmed = value.trim();
  if (trimmed.length === 0 || trimmed.length > LOGO_URL_MAX) return false;
  try {
    const url = new URL(trimmed);
    // https-only AND no embedded credentials: a `https://user:pass@host/x` logo would leak those
    // creds to every visitor's browser (visible in the rendered src + sent on the cross-origin fetch).
    // Never legitimate for a logo — reject as a phishing/credential-leak vector.
    return url.protocol === 'https:' && url.username === '' && url.password === '';
  } catch {
    return false;
  }
}

export interface SiteIdentity {
  name: string;
  logoUrl: string | null;
}

/**
 * Parse the stored `site_name` + `site_logo_url` settings into a render-safe identity. ALWAYS resolves:
 * name falls back to DEFAULT_SITE_NAME (absent/blank/non-string) and is length-capped; logoUrl is the
 * stored value only when it passes `isValidLogoUrl`, else null. Never throws — the header never breaks
 * on a malformed/tampered setting (matches parseHomepage's fail-closed contract).
 */
export function parseSiteIdentity(rawName: unknown, rawLogoUrl: unknown): SiteIdentity {
  let name = DEFAULT_SITE_NAME;
  if (typeof rawName === 'string') {
    const trimmed = rawName.trim();
    if (trimmed.length > 0) name = trimmed.slice(0, SITE_NAME_MAX);
  }
  const logoUrl = isValidLogoUrl(rawLogoUrl) ? (rawLogoUrl as string).trim() : null;
  return { name, logoUrl };
}

const MAINTENANCE_MSG_MAX = 2000;
const MAINTENANCE_ROLES_MAX = 20;
const RETRY_AFTER_MIN = 1;
const RETRY_AFTER_MAX = 86400; // 24h

/**
 * Parse the stored `maintenance` setting into a `MaintenanceConfig`. ALWAYS resolves — any
 * malformed/absent value falls back to `{ enabled: false }`. A throw here would defeat the
 * operator escape hatch (a crashing per-request guard locks EVERYONE out), so every field is
 * coerced fail-closed. `enabled` requires a STRICT boolean true (truthy junk never enables).
 */
export function parseMaintenance(value: unknown): MaintenanceConfig {
  if (!value || typeof value !== 'object') return { enabled: false };
  const v = value as Record<string, unknown>;
  if (v.enabled !== true) return { enabled: false };

  const cfg: MaintenanceConfig = { enabled: true };

  if (typeof v.message === 'string') {
    const msg = v.message.trim().slice(0, MAINTENANCE_MSG_MAX);
    if (msg.length > 0) cfg.message = msg;
  }
  if (Array.isArray(v.allowRoles)) {
    const roles = v.allowRoles
      .filter((r): r is string => typeof r === 'string' && r.trim().length > 0)
      .map((r) => r.trim())
      .slice(0, MAINTENANCE_ROLES_MAX);
    if (roles.length > 0) cfg.allowRoles = roles;
  }
  if (typeof v.retryAfterSec === 'number' && Number.isFinite(v.retryAfterSec)) {
    const n = Math.floor(v.retryAfterSec);
    if (n >= RETRY_AFTER_MIN) cfg.retryAfterSec = Math.min(n, RETRY_AFTER_MAX);
  }
  return cfg;
}

const CONSENT_POLICY_HREF_MAX = 2048;
const CONSENT_VERSION_MAX = 64;
const DEFAULT_POLICY_HREF = '/privacy';
// The non-necessary categories the banner may show (matches ConsentBanner's DEFAULT_CATEGORIES).
// `necessary` is always-on and never a togglable banner category, so it is excluded here.
const NON_NECESSARY_CATEGORIES: ConsentCategory[] = ['analytics', 'marketing', 'preferences'];

export type ConsentConfig =
  | { enabled: false }
  | { enabled: true; version: string; policyHref: string; categories: ConsentCategory[] };

/**
 * Accept a relative href (no scheme) or an http(s)/mailto absolute href; anything else → DEFAULT_POLICY_HREF.
 * Tests on a control-char-stripped copy (browsers drop ASCII whitespace/control chars before parsing the
 * scheme, so `java\tscript:` cannot bypass). Defense-in-depth — ConsentBanner ALSO guards the href at render.
 */
function safeConsentHref(value: unknown): string {
  if (typeof value !== 'string') return DEFAULT_POLICY_HREF;
  const trimmed = value.trim();
  if (trimmed.length === 0 || trimmed.length > CONSENT_POLICY_HREF_MAX) return DEFAULT_POLICY_HREF;
  const cleaned = trimmed.replace(/[\u0000-\u0020]+/g, '');
  const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(cleaned);
  if (!scheme) return trimmed; // no scheme → relative/anchor/query → safe
  const s = scheme[1]!.toLowerCase();
  return s === 'http' || s === 'https' || s === 'mailto' ? trimmed : DEFAULT_POLICY_HREF;
}

/**
 * Parse the stored `consent` setting into a typed config. ALWAYS resolves — any malformed/absent value
 * (or `enabled !== true`) falls back to `{ enabled: false }` (no banner). When enabled, fills `version`
 * (default '0'), `policyHref` (default /privacy, scheme-guarded), and `categories` (filtered to the
 * non-necessary allowlist). Never throws — the public shell must never break on a tampered setting
 * (matches parseMaintenance/parseHomepage fail-closed contract).
 */
export function parseConsentConfig(value: unknown): ConsentConfig {
  if (!value || typeof value !== 'object') return { enabled: false };
  const v = value as Record<string, unknown>;
  if (v.enabled !== true) return { enabled: false };
  const version =
    typeof v.version === 'string' && v.version.trim().length > 0
      ? v.version.trim().slice(0, CONSENT_VERSION_MAX)
      : '0';
  const policyHref = safeConsentHref(v.policyHref);
  const categories = Array.isArray(v.categories)
    ? v.categories.filter(
        (c): c is ConsentCategory =>
          typeof c === 'string' && NON_NECESSARY_CATEGORIES.includes(c as ConsentCategory),
      )
    : [...NON_NECESSARY_CATEGORIES];
  return { enabled: true, version, policyHref, categories };
}

const DISCUSSION_NESTING_MAX = 10;
const DISCUSSION_BLOCKLIST_MAX = 200;
const DISCUSSION_BLOCKLIST_TERM_MAX = 100;

export type DiscussionConfig = {
  enabled: boolean;
  whoCanComment: 'everyone' | 'registered';
  nestingDepth: number;
  blocklist: string[];
};

const DISCUSSION_DEFAULTS: DiscussionConfig = {
  enabled: false,
  whoCanComment: 'everyone',
  nestingDepth: 3,
  blocklist: [],
};

/**
 * Parse the stored `discussion` setting into a typed config. ALWAYS resolves — any malformed/absent
 * value falls back to DISCUSSION_DEFAULTS. Never throws (matches parseMaintenance fail-closed contract).
 */
export function parseDiscussionConfig(value: unknown): DiscussionConfig {
  if (!value || typeof value !== 'object') return { ...DISCUSSION_DEFAULTS };

  const v = value as Record<string, unknown>;

  const enabled = v.enabled === true;

  const whoCanComment =
    v.whoCanComment === 'registered' ? 'registered' : 'everyone';

  let nestingDepth = 3;
  if (typeof v.nestingDepth === 'number' && Number.isFinite(v.nestingDepth)) {
    nestingDepth = Math.min(
      DISCUSSION_NESTING_MAX,
      Math.max(0, Math.round(v.nestingDepth)),
    );
  }

  let blocklist: string[] = [];
  if (Array.isArray(v.blocklist)) {
    const seen = new Set<string>();
    for (const item of v.blocklist) {
      if (typeof item !== 'string') continue;
      const term = item.trim().toLowerCase().slice(0, DISCUSSION_BLOCKLIST_TERM_MAX);
      if (term.length === 0 || seen.has(term)) continue;
      seen.add(term);
      blocklist.push(term);
      if (blocklist.length >= DISCUSSION_BLOCKLIST_MAX) break;
    }
  }

  return { enabled, whoCanComment, nestingDepth, blocklist };
}

/**
 * Parse newline-separated blocklist input from the admin form into a normalized string array.
 * Trims, lowercases, deduplicates (preserving order), and caps at DISCUSSION_BLOCKLIST_MAX.
 */
export function parseBlocklistInput(raw: string): string[] {
  const seen = new Set<string>();
  const result: string[] = [];
  for (const segment of raw.split('\n')) {
    const term = segment.trim().toLowerCase();
    if (term.length === 0 || seen.has(term)) continue;
    seen.add(term);
    result.push(term);
    if (result.length >= DISCUSSION_BLOCKLIST_MAX) break;
  }
  return result;
}
