// Chat widget behavior: the app's controls (Widget show trigger, Greeting,
// Half-sized, Step aside, Open chat widget on desktop) mapped to and from
// Stand's Behavior rule fields, the same way the Stand app's Sites page does.
import type { BehaviorRule } from './types.ts';

export type TriggerMode = 'onLoad' | 'time' | 'scroll' | 'none';
export type GreetingMode = 'default' | 'none' | 'custom';

export const MAX_RULES = 20;
export const DEFAULT_DELAY_MS = 5000;
export const MAX_DELAY_SECONDS = 60;

export function newRule(pathPrefix = '', name = ''): BehaviorRule {
  return {
    ruleId: `rule-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
    name: name || 'Behavior rule',
    enabled: true,
    pathPrefix,
    hideButtonUntilTriggered: false,
    presentation: { noGreetings: false, halfSize: false, stepAside: false },
    trigger: { type: 'immediate', delayMs: 0, scrollPercent: 70 },
    action: { type: 'showButton', initialMessage: '' },
  };
}

export function triggerMode(rule: BehaviorRule): TriggerMode {
  if (rule.trigger.type === 'delay') return 'time';
  if (rule.trigger.type === 'firstScroll' || rule.trigger.type === 'scrollDepth') return 'scroll';
  if (rule.trigger.type === 'none') return 'none';
  return 'onLoad';
}

export function clampScrollPercent(value: unknown): number {
  return Math.max(1, Math.min(100, Math.trunc(Number(value) || 70)));
}

export function clampDelaySeconds(value: unknown): number {
  return Math.max(0, Math.min(MAX_DELAY_SECONDS, Math.trunc(Number(value) || 0)));
}

export function withTriggerMode(rule: BehaviorRule, mode: TriggerMode): BehaviorRule {
  const scrollPercent = clampScrollPercent(rule.trigger.scrollPercent);
  switch (mode) {
    case 'time':
      return {
        ...rule,
        hideButtonUntilTriggered: true,
        trigger: { type: 'delay', delayMs: rule.trigger.delayMs > 0 ? rule.trigger.delayMs : DEFAULT_DELAY_MS, scrollPercent },
      };
    case 'scroll':
      return { ...rule, hideButtonUntilTriggered: true, trigger: { type: 'scrollDepth', delayMs: 0, scrollPercent } };
    case 'none':
      return { ...rule, hideButtonUntilTriggered: true, trigger: { type: 'none', delayMs: 0, scrollPercent } };
    default:
      return { ...rule, hideButtonUntilTriggered: false, trigger: { type: 'immediate', delayMs: 0, scrollPercent } };
  }
}

export function greetingMode(rule: BehaviorRule, customRuleIds: ReadonlySet<string> = new Set()): GreetingMode {
  if (rule.presentation.noGreetings) return 'none';
  if (customRuleIds.has(rule.ruleId) || rule.action.initialMessage.trim()) return 'custom';
  return 'default';
}

export function withGreetingMode(rule: BehaviorRule, mode: GreetingMode): BehaviorRule {
  return {
    ...rule,
    presentation: { ...rule.presentation, noGreetings: mode === 'none' },
    action: {
      ...rule.action,
      // Opening the chat widget needs a greeting.
      type: mode !== 'none' && rule.action.type === 'openChatAfterGreeting' ? 'openChatAfterGreeting' : 'showButton',
      initialMessage: mode === 'custom' ? rule.action.initialMessage : '',
    },
  };
}

export function withOpenOnDesktop(rule: BehaviorRule, enabled: boolean): BehaviorRule {
  return { ...rule, action: { ...rule.action, type: enabled ? 'openChatAfterGreeting' : 'showButton' } };
}

/** Mirrors Stand's path matching (SitePathMatcher). */
export function pathMatches(pattern: string, path: string): boolean {
  const trimmed = pattern.trim();
  if (trimmed === '') return true;
  const normalized = path === '' ? '/' : path.startsWith('/') ? path : `/${path}`;
  if (trimmed.endsWith('/*')) {
    const base = trimSlash(trimmed.slice(0, -2));
    return base === '/' || trimSlash(normalized) === base || normalized.startsWith(`${base}/`);
  }
  return trimSlash(normalized) === trimSlash(trimmed);
}

export function trimSlash(path: string): string {
  let value = path.startsWith('/') ? path : `/${path}`;
  while (value.length > 1 && value.endsWith('/')) value = value.slice(0, -1);
  return value;
}

/** Index of the rule Stand applies on a path: the first enabled match. */
export function firstMatch(rules: BehaviorRule[], path: string): number {
  return rules.findIndex((rule) => rule.enabled && pathMatches(rule.pathPrefix, path));
}

/**
 * Where a new rule for a pattern goes: above the first rule that also matches
 * its pages, so single pages stay above whole sections, and above the rule for
 * all other pages.
 */
export function insertionIndex(rules: BehaviorRule[], pattern: string): number {
  const probe = pattern.endsWith('/*') ? pattern.slice(0, -2) || '/' : pattern;
  const index = rules.findIndex((rule) => rule.pathPrefix === '' || pathMatches(rule.pathPrefix, probe));
  return index === -1 ? rules.length : index;
}

export function insertRule(rules: BehaviorRule[], rule: BehaviorRule): BehaviorRule[] {
  const next = [...rules];
  next.splice(insertionIndex(rules, rule.pathPrefix), 0, rule);
  return next;
}

/** Keeps the rule for all other pages last; Stand never reaches rules below it. */
export function keepDefaultLast(rules: BehaviorRule[]): BehaviorRule[] {
  return [...rules.filter((rule) => rule.pathPrefix !== ''), ...rules.filter((rule) => rule.pathPrefix === '')];
}

export function moveRule(rules: BehaviorRule[], index: number, direction: -1 | 1): BehaviorRule[] {
  const target = index + direction;
  if (target < 0 || target >= rules.length) return rules;
  if (rules[index].pathPrefix === '' || rules[target].pathPrefix === '') return rules;
  const next = [...rules];
  [next[index], next[target]] = [next[target], next[index]];
  return next;
}
