/**
 * Shared, dependency-free helpers for care services.
 *
 * These utilities deliberately stay internal to the care module: they provide
 * defensive JSON parsing, chat-response extraction, request timeouts, stable
 * hashing, and calendar-date arithmetic without changing any public service API.
 */

declare const process: {
  env: {
    EXPO_PUBLIC_PROXY_URL?: string;
    EXPO_PUBLIC_APP_TOKEN?: string;
  };
} | undefined;

export type JsonRecord = Record<string, unknown>;

const configuredProxyEndpoint = typeof process !== 'undefined'
  ? process.env.EXPO_PUBLIC_PROXY_URL
  : undefined;

export const DEFAULT_PROXY_ENDPOINT = configuredProxyEndpoint
  || 'https://pixiesprout-ai.46-4-121-190.sslip.io/v1/chat/completions';

const MS_PER_DAY = 86_400_000;
const DATE_KEY_RE = /^(\d{4})-(\d{2})-(\d{2})(?:$|T)/;

export function getProxyToken(): string {
  const token = typeof process !== 'undefined'
    ? process.env.EXPO_PUBLIC_APP_TOKEN
    : undefined;
  return typeof token === 'string' ? token.trim() : '';
}

export function isRecord(value: unknown): value is JsonRecord {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

export function asTrimmedString(value: unknown, maxLength?: number): string | undefined {
  if (typeof value !== 'string') return undefined;
  const trimmed = value.trim();
  if (!trimmed) return undefined;
  return typeof maxLength === 'number' && maxLength >= 0
    ? trimmed.slice(0, maxLength)
    : trimmed;
}

/** Collapse user/model prose to one prompt-safe line without changing words. */
export function asInlineText(value: unknown, maxLength = 1_000): string | undefined {
  const text = asTrimmedString(value);
  if (!text) return undefined;
  return text.replace(/\s+/g, ' ').slice(0, Math.max(0, maxLength));
}

/** Accept finite numbers and non-blank numeric strings; reject null/booleans/blanks. */
export function asFiniteNumber(value: unknown): number | null {
  if (typeof value === 'number') return Number.isFinite(value) ? value : null;
  if (typeof value !== 'string' || !value.trim()) return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

export function uniqueStrings(values: Iterable<unknown>): string[] {
  const out: string[] = [];
  const seen = new Set<string>();
  for (const value of values) {
    const text = asTrimmedString(value);
    if (!text || seen.has(text)) continue;
    seen.add(text);
    out.push(text);
  }
  return out;
}

/** djb2: deterministic change/dedup hash, not a security primitive. */
export function hashString(input: string): string {
  let hash = 5381;
  for (let i = 0; i < input.length; i += 1) {
    hash = ((hash << 5) + hash + input.charCodeAt(i)) >>> 0;
  }
  return hash.toString(36);
}

/**
 * Extract the first complete JSON object from a model response. The scanner is
 * string/escape aware, so braces inside JSON strings do not terminate early.
 */
export function extractJsonObjectText(content: string): string {
  const trimmed = content.trim();
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
  const sources = fenced?.[1]?.trim() && fenced[1].trim() !== trimmed
    ? [fenced[1].trim(), trimmed]
    : [trimmed];

  for (const source of sources) {
    // Try every opening brace. This recovers when a model emits stray prose
    // containing an unmatched "{" before the actual JSON object.
    for (let start = source.indexOf('{'); start >= 0; start = source.indexOf('{', start + 1)) {
      let depth = 0;
      let inString = false;
      let escaped = false;
      for (let i = start; i < source.length; i += 1) {
        const char = source[i];
        if (inString) {
          if (escaped) {
            escaped = false;
          } else if (char === '\\') {
            escaped = true;
          } else if (char === '"') {
            inString = false;
          }
          continue;
        }
        if (char === '"') {
          inString = true;
        } else if (char === '{') {
          depth += 1;
        } else if (char === '}') {
          depth -= 1;
          if (depth === 0) {
            const candidate = source.slice(start, i + 1);
            try {
              const parsed: unknown = JSON.parse(candidate);
              if (isRecord(parsed)) return candidate;
            } catch {
              // Try the next opening brace; this one was balanced prose or
              // otherwise not a complete JSON object.
            }
            break;
          }
        }
      }
    }
  }
  throw new Error(trimmed.includes('{') ? 'Incomplete JSON object' : 'No JSON object found');
}

export function parseJsonObject(content: string): JsonRecord | null {
  try {
    const parsed: unknown = JSON.parse(extractJsonObjectText(content));
    return isRecord(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

function messageContentToString(value: unknown): string | undefined {
  if (typeof value === 'string') return asTrimmedString(value);
  if (!Array.isArray(value)) return undefined;

  const parts: string[] = [];
  for (const item of value) {
    if (!isRecord(item)) continue;
    const text = asTrimmedString(item.text);
    if (text) parts.push(text);
  }
  return parts.length ? parts.join('\n') : undefined;
}

/** Read a successful, non-truncated OpenAI-compatible chat response. */
export async function readChatCompletionContent(response: Response): Promise<string | null> {
  if (!response.ok) return null;
  const data: unknown = await response.json();
  if (!isRecord(data) || !Array.isArray(data.choices)) return null;
  const choice = data.choices[0];
  if (!isRecord(choice) || !isRecord(choice.message)) return null;
  if (typeof choice.finish_reason === 'string' && choice.finish_reason !== 'stop') return null;
  return messageContentToString(choice.message.content) || null;
}

/** Start the timeout when the actual network operation starts, not while queued. */
export async function fetchWithTimeout(
  request: (signal: AbortSignal) => Promise<Response>,
  timeoutMs: number,
): Promise<Response> {
  const controller = new AbortController();
  const safeTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 30_000;
  const timeoutId = setTimeout(() => controller.abort(), safeTimeout);
  try {
    return await request(controller.signal);
  } finally {
    clearTimeout(timeoutId);
  }
}

export function normalizeDateKey(value: unknown): string | null {
  if (typeof value !== 'string') return null;
  const match = DATE_KEY_RE.exec(value.trim());
  if (!match) return null;
  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  // Date.UTC treats years 0-99 as 1900-1999. Set the year explicitly so the
  // normalizer remains mathematically correct for every four-digit date key.
  const date = new Date(0);
  date.setUTCHours(0, 0, 0, 0);
  date.setUTCFullYear(year, month - 1, day);
  if (
    date.getUTCFullYear() !== year ||
    date.getUTCMonth() !== month - 1 ||
    date.getUTCDate() !== day
  ) {
    return null;
  }
  return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}

export function dateKeyOrdinal(value: unknown): number | null {
  const key = normalizeDateKey(value);
  if (!key) return null;
  const [year, month, day] = key.split('-').map(Number);
  const date = new Date(0);
  date.setUTCHours(0, 0, 0, 0);
  date.setUTCFullYear(year, month - 1, day);
  return Math.trunc(date.getTime() / MS_PER_DAY);
}

export function localTodayDateKey(now = new Date()): string {
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const day = String(now.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

/** Signed calendar-day distance: target minus local today. */
export function daysUntilLocalDate(value: unknown, now = new Date()): number | null {
  const target = dateKeyOrdinal(value);
  const today = dateKeyOrdinal(localTodayDateKey(now));
  return target === null || today === null ? null : target - today;
}

/** Signed calendar-day distance: later minus earlier. */
export function daysBetweenDateKeys(later: unknown, earlier: unknown): number | null {
  const laterOrdinal = dateKeyOrdinal(later);
  const earlierOrdinal = dateKeyOrdinal(earlier);
  return laterOrdinal === null || earlierOrdinal === null
    ? null
    : laterOrdinal - earlierOrdinal;
}

export function addDaysToDateKey(value: unknown, days: number): string | null {
  const key = normalizeDateKey(value);
  if (!key || !Number.isFinite(days)) return null;
  const [year, month, day] = key.split('-').map(Number);
  const date = new Date(0);
  date.setUTCHours(0, 0, 0, 0);
  date.setUTCFullYear(year, month - 1, day + Math.trunc(days));
  return `${String(date.getUTCFullYear()).padStart(4, '0')}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;
}

export function formatDateKey(value: unknown): string | null {
  const key = normalizeDateKey(value);
  if (!key) return null;
  const [, month, day] = key.split('-').map(Number);
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  return `${months[month - 1]} ${day}`;
}
