import { join, isAbsolute } from "path";
import { mkdir } from "fs/promises";
import { existsSync } from "fs";
import { normalizeTimezoneName, resolveTimezoneOffsetMinutes } from "./timezone";

const HEARTBEAT_DIR = join(process.cwd(), ".qwen", "qwenclaw");
const SETTINGS_FILE = join(HEARTBEAT_DIR, "settings.json");
const JOBS_DIR = join(HEARTBEAT_DIR, "jobs");
const LOGS_DIR = join(HEARTBEAT_DIR, "logs");

const DEFAULT_SETTINGS: Settings = {
  model: "",
  api: "",
  fallback: {
    model: "",
    api: "",
  },
  timezone: "UTC",
  timezoneOffsetMinutes: 0,
  heartbeat: {
    enabled: false,
    interval: 15,
    prompt: "",
    excludeWindows: [],
  },
  telegram: {
    token: "",
    allowedUserIds: [],
  },
  security: {
    level: "moderate",
    allowedTools: [],
    disallowedTools: [],
  },
  web: {
    enabled: false,
    host: "127.0.0.1",
    port: 4632,
  },
};

export interface HeartbeatExcludeWindow {
  days?: number[];
  start: string;
  end: string;
}

export interface HeartbeatConfig {
  enabled: boolean;
  interval: number;
  prompt: string;
  excludeWindows: HeartbeatExcludeWindow[];
}

export interface TelegramConfig {
  token: string;
  allowedUserIds: number[];
}

export type SecurityLevel =
  | "locked"
  | "strict"
  | "moderate"
  | "unrestricted";

export interface SecurityConfig {
  level: SecurityLevel;
  allowedTools: string[];
  disallowedTools: string[];
}

export interface Settings {
  model: string;
  api: string;
  fallback: ModelConfig;
  timezone: string;
  timezoneOffsetMinutes: number;
  heartbeat: HeartbeatConfig;
  telegram: TelegramConfig;
  security: SecurityConfig;
  web: WebConfig;
}

export interface ModelConfig {
  model: string;
  api: string;
}

export interface WebConfig {
  enabled: boolean;
  host: string;
  port: number;
}

let cached: Settings | null = null;

export async function initConfig(): Promise<void> {
  await mkdir(HEARTBEAT_DIR, { recursive: true });
  await mkdir(JOBS_DIR, { recursive: true });
  await mkdir(LOGS_DIR, { recursive: true });
  if (!existsSync(SETTINGS_FILE)) {
    await Bun.write(SETTINGS_FILE, JSON.stringify(DEFAULT_SETTINGS, null, 2) + "\n");
  }
}

const VALID_LEVELS = new Set([
  "locked",
  "strict",
  "moderate",
  "unrestricted",
]);

function parseSettings(raw: Record<string, unknown>): Settings {
  const securityRaw = raw.security as Record<string, unknown> | undefined;
  const rawLevel = securityRaw?.level;
  const level: SecurityLevel =
    typeof rawLevel === "string" && VALID_LEVELS.has(rawLevel as SecurityLevel)
      ? (rawLevel as SecurityLevel)
      : "moderate";

  const parsedTimezone = parseTimezone(raw.timezone);

  const heartbeatRaw = raw.heartbeat as Record<string, unknown> | undefined;
  const telegramRaw = raw.telegram as Record<string, unknown> | undefined;
  const securityConfigRaw = raw.security as Record<string, unknown> | undefined;
  const webRaw = raw.web as Record<string, unknown> | undefined;
  const fallbackRaw = raw.fallback as Record<string, unknown> | undefined;

  return {
    model: typeof raw.model === "string" ? raw.model.trim() : "",
    api: typeof raw.api === "string" ? raw.api.trim() : "",
    fallback: {
      model:
        typeof fallbackRaw?.model === "string" ? fallbackRaw.model.trim() : "",
      api:
        typeof fallbackRaw?.api === "string" ? fallbackRaw.api.trim() : "",
    },
    timezone: parsedTimezone,
    timezoneOffsetMinutes: parseTimezoneOffsetMinutes(
      raw.timezoneOffsetMinutes,
      parsedTimezone
    ),
    heartbeat: {
      enabled: Boolean(heartbeatRaw?.enabled),
      interval: Number(heartbeatRaw?.interval) || 15,
      prompt: String(heartbeatRaw?.prompt ?? ""),
      excludeWindows: parseExcludeWindows(heartbeatRaw?.excludeWindows),
    },
    telegram: {
      token: String(telegramRaw?.token ?? ""),
      allowedUserIds: Array.isArray(telegramRaw?.allowedUserIds) ? telegramRaw.allowedUserIds as number[] : [],
    },
    security: {
      level,
      allowedTools: Array.isArray(securityConfigRaw?.allowedTools)
        ? securityConfigRaw.allowedTools as string[]
        : [],
      disallowedTools: Array.isArray(securityConfigRaw?.disallowedTools)
        ? securityConfigRaw.disallowedTools as string[]
        : [],
    },
    web: {
      enabled: Boolean(webRaw?.enabled),
      host: String(webRaw?.host ?? "127.0.0.1"),
      port: webRaw && Number.isFinite(webRaw.port) ? Number(webRaw.port) : 4632,
    },
  };
}

const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
const ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];

function parseTimezone(value: unknown): string {
  return normalizeTimezoneName(value);
}

function parseExcludeWindows(value: unknown): HeartbeatExcludeWindow[] {
  if (!Array.isArray(value)) return [];
  const out: HeartbeatExcludeWindow[] = [];
  for (const entry of value) {
    if (!entry || typeof entry !== "object") continue;
    const start =
      typeof (entry as any).start === "string" ? (entry as any).start.trim() : "";
    const end =
      typeof (entry as any).end === "string" ? (entry as any).end.trim() : "";
    if (!TIME_RE.test(start) || !TIME_RE.test(end)) continue;
    const rawDays = Array.isArray((entry as any).days) ? (entry as any).days : [];
    const parsedDays = rawDays
      .map((d: any) => Number(d))
      .filter((d: any): d is number => Number.isInteger(d) && d >= 0 && d <= 6);
    const uniqueDays = Array.from(new Set(parsedDays)) as number[];
    uniqueDays.sort((a, b) => a - b);
    out.push({
      start,
      end,
      days: uniqueDays.length > 0 ? uniqueDays : [...ALL_DAYS],
    });
  }
  return out;
}

function parseTimezoneOffsetMinutes(
  value: unknown,
  timezoneFallback?: string
): number {
  return resolveTimezoneOffsetMinutes(value, timezoneFallback);
}

export async function loadSettings(): Promise<Settings> {
  if (cached) return cached;
  const raw = await Bun.file(SETTINGS_FILE).json();
  cached = parseSettings(raw);
  return cached;
}

/** Re-read settings from disk, bypassing cache. */
export async function reloadSettings(): Promise<Settings> {
  const raw = await Bun.file(SETTINGS_FILE).json();
  cached = parseSettings(raw);
  return cached;
}

export function getSettings(): Settings {
  if (!cached)
    throw new Error("Settings not loaded. Call loadSettings() first.");
  return cached;
}

const PROMPT_EXTENSIONS = [".md", ".txt", ".prompt"];

/**
 * If the prompt string looks like a file path (ends with .md, .txt, or .prompt),
 * read and return the file contents. Otherwise return the string as-is.
 * Relative paths are resolved from the project root (cwd).
 */
export async function resolvePrompt(prompt: string): Promise<string> {
  const trimmed = prompt.trim();
  if (!trimmed) return trimmed;
  const isPath = PROMPT_EXTENSIONS.some((ext) => trimmed.endsWith(ext));
  if (!isPath) return trimmed;
  const resolved = isAbsolute(trimmed) ? trimmed : join(process.cwd(), trimmed);
  try {
    const content = await Bun.file(resolved).text();
    return content.trim();
  } catch {
    console.warn(
      `[config] Prompt path "${trimmed}" not found, using as literal string`
    );
    return trimmed;
  }
}
