import { mkdir, readFile, writeFile } from "fs/promises";
import { join } from "path";
import { existsSync } from "fs";
import { getSession, createSession } from "./sessions";
import { getSettings, type ModelConfig, type SecurityConfig } from "./config";
import { buildClockPromptPrefix } from "./timezone";

const LOGS_DIR = join(process.cwd(), ".qwen/qwenclaw/logs");
// Resolve prompts relative to the qwenclaw installation, not the project dir
const PROMPTS_DIR = join(import.meta.dir, "..", "prompts");
const HEARTBEAT_PROMPT_FILE = join(PROMPTS_DIR, "heartbeat", "HEARTBEAT.md");
const PROJECT_QWEN_MD = join(process.cwd(), "QWEN.md");
const LEGACY_PROJECT_QWEN_MD = join(process.cwd(), ".qwen", "QWEN.md");
const QWENCLAW_BLOCK_START = "<!-- QWENCLAW_MANAGED_BLOCK_START -->";
const QWENCLAW_BLOCK_END = "<!-- QWENCLAW_MANAGED_BLOCK_END -->";

export interface RunResult {
  stdout: string;
  stderr: string;
  exitCode: number;
}

const RATE_LIMIT_PATTERN = /you(?:'|')ve hit your limit/i;

// Serial queue — prevents concurrent --resume on the same session
let queue: Promise<unknown> = Promise.resolve();
function enqueue<T>(fn: () => Promise<T>): Promise<T> {
  const task = queue.then(fn, fn);
  queue = task.catch((): Promise<unknown> => Promise.resolve());
  return task;
}

function extractRateLimitMessage(stdout: string, stderr: string): string | null {
  const candidates = [stdout, stderr];
  for (const text of candidates) {
    const trimmed = text.trim();
    if (trimmed && RATE_LIMIT_PATTERN.test(trimmed)) return trimmed;
  }
  return null;
}

function sameModelConfig(a: ModelConfig, b: ModelConfig): boolean {
  return a.model.trim().toLowerCase() === b.model.trim().toLowerCase() && a.api.trim() === b.api.trim();
}

function hasModelConfig(value: ModelConfig): boolean {
  return value.model.trim().length > 0 || value.api.trim().length > 0;
}

function buildChildEnv(
  baseEnv: Record<string, string>,
  model: string,
  api: string
): Record<string, string> {
  const childEnv: Record<string, string> = { ...baseEnv };
  if (api.trim()) childEnv.ANTHROPIC_AUTH_TOKEN = api.trim();
  // Add any Qwen-specific environment variables here
  return childEnv;
}

async function runQwenOnce(
  baseArgs: string[],
  model: string,
  api: string,
  baseEnv: Record<string, string>
): Promise<{ rawStdout: string; stderr: string; exitCode: number }> {
  const args = [...baseArgs];
  if (model.trim()) args.push("--model", model.trim());

  // Cross-platform command detection
  const qwenCommand = process.platform === "win32" 
    ? "qwen.cmd" 
    : process.platform === "darwin" 
      ? "qwen" 
      : "qwen";
  
  const proc = Bun.spawn([qwenCommand, ...args], {
    stdout: "pipe",
    stderr: "pipe",
    env: buildChildEnv(baseEnv, model, api),
    shell: true,
    // Detach on non-Windows to allow background execution
    detached: process.platform !== "win32",
  });

  const [rawStdout, stderr] = await Promise.all([
    new Response(proc.stdout).text(),
    new Response(proc.stderr).text(),
  ]);

  await proc.exited;
  return { rawStdout, stderr, exitCode: proc.exitCode ?? 1 };
}

const PROJECT_DIR = process.cwd();
const DIR_SCOPE_PROMPT = [
  `CRITICAL SECURITY CONSTRAINT: You are scoped to the project directory: ${PROJECT_DIR}`,
  "You MUST NOT read, write, edit, or delete any file outside this directory.",
  "You MUST NOT run bash commands that modify anything outside this directory (no cd /, no /etc, no ~/, no ../.. escapes).",
  "If a request requires accessing files outside the project, refuse and explain why.",
].join("\n");

export async function ensureProjectQwenMd(): Promise<void> {
  // Preflight-only initialization: never rewrite an existing project QWEN.md.
  if (existsSync(PROJECT_QWEN_MD)) return;

  const promptContent = (await loadPrompts()).trim();
  const managedBlock = [
    QWENCLAW_BLOCK_START,
    promptContent,
    QWENCLAW_BLOCK_END,
  ].join("\n");

  let content = "";
  if (existsSync(LEGACY_PROJECT_QWEN_MD)) {
    try {
      const legacy = await readFile(LEGACY_PROJECT_QWEN_MD, "utf8");
      content = legacy.trim();
    } catch (e) {
      console.error(`[${new Date().toLocaleTimeString()}] Failed to read legacy .qwen/QWEN.md:`, e);
      return;
    }
  }

  const normalized = content.trim();
  const hasManagedBlock =
    normalized.includes(QWENCLAW_BLOCK_START) &&
    normalized.includes(QWENCLAW_BLOCK_END);

  const managedPattern = new RegExp(
    `${QWENCLAW_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${QWENCLAW_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
    "m"
  );

  const merged = hasManagedBlock
    ? `${normalized.replace(managedPattern, managedBlock)}\n`
    : normalized
    ? `${normalized}\n\n${managedBlock}\n`
    : `${managedBlock}\n`;

  try {
    await writeFile(PROJECT_QWEN_MD, merged, "utf8");
  } catch (e) {
    console.error(`[${new Date().toLocaleTimeString()}] Failed to write project QWEN.md:`, e);
  }
}

function buildSecurityArgs(security: SecurityConfig): string[] {
  const args: string[] = [];

  // Qwen-specific security flags - adjust based on Qwen's CLI options
  if (security.allowedTools.length > 0) {
    args.push("--allowed-tools", security.allowedTools.join(","));
  }

  if (security.disallowedTools.length > 0) {
    args.push("--disallowed-tools", security.disallowedTools.join(","));
  }

  return args;
}

/** Load and concatenate all prompt files from the prompts/ directory. */
async function loadPrompts(): Promise<string> {
  const selectedPromptFiles = [
    join(PROMPTS_DIR, "IDENTITY.md"),
    join(PROMPTS_DIR, "USER.md"),
    join(PROMPTS_DIR, "SOUL.md"),
  ];

  const parts: string[] = [];
  for (const file of selectedPromptFiles) {
    try {
      const content = await Bun.file(file).text();
      if (content.trim()) parts.push(content.trim());
    } catch (e) {
      console.error(`[${new Date().toLocaleTimeString()}] Failed to read prompt file ${file}:`, e);
    }
  }

  return parts.join("\n\n");
}

export async function loadHeartbeatPromptTemplate(): Promise<string> {
  try {
    const content = await Bun.file(HEARTBEAT_PROMPT_FILE).text();
    return content.trim();
  } catch {
    return "";
  }
}

async function execQwen(name: string, prompt: string): Promise<RunResult> {
  await mkdir(LOGS_DIR, { recursive: true });

  const existing = await getSession();
  const isNew = !existing;
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  const logFile = join(LOGS_DIR, `${name}-${timestamp}.log`);

  const { security, model, api, fallback } = getSettings();
  const primaryConfig: ModelConfig = { model, api };
  const fallbackConfig: ModelConfig = {
    model: fallback?.model ?? "",
    api: fallback?.api ?? "",
  };

  const securityArgs = buildSecurityArgs(security);

  console.log(
    `[${new Date().toLocaleTimeString()}] Running: ${name} (${isNew ? "new session" : `resume ${existing.sessionId.slice(0, 8)}`}, security: ${security.level})`
  );

  // Build the base command for Qwen
  const args = [
    "qwen",
    "-p",
    prompt,
    ...securityArgs,
  ];

  if (!isNew) {
    args.push("--resume", existing.sessionId);
  }

  // Build the appended system prompt: prompt files + directory scoping
  const promptContent = await loadPrompts();
  const appendParts: string[] = ["You are running inside QwenClaw."];
  if (promptContent) appendParts.push(promptContent);

  // Load the project's QWEN.md if it exists
  if (existsSync(PROJECT_QWEN_MD)) {
    try {
      const qwenMd = await Bun.file(PROJECT_QWEN_MD).text();
      if (qwenMd.trim()) appendParts.push(qwenMd.trim());
    } catch (e) {
      console.error(`[${new Date().toLocaleTimeString()}] Failed to read project QWEN.md:`, e);
    }
  }

  if (security.level !== "unrestricted") appendParts.push(DIR_SCOPE_PROMPT);

  if (appendParts.length > 0) {
    args.push("--system-prompt", appendParts.join("\n\n"));
  }

  // Strip any nested env vars
  const { QWEN_CODE: _, ...cleanEnv } = process.env;
  const baseEnv = { ...cleanEnv } as Record<string, string>;

  let exec = await runQwenOnce(args, primaryConfig.model, primaryConfig.api, baseEnv);
  const primaryRateLimit = extractRateLimitMessage(exec.rawStdout, exec.stderr);

  let usedFallback = false;
  if (
    primaryRateLimit &&
    hasModelConfig(fallbackConfig) &&
    !sameModelConfig(primaryConfig, fallbackConfig)
  ) {
    console.warn(
      `[${new Date().toLocaleTimeString()}] Qwen limit reached; retrying with fallback${fallbackConfig.model ? ` (${fallbackConfig.model})` : ""}...`
    );
    exec = await runQwenOnce(args, fallbackConfig.model, fallbackConfig.api, baseEnv);
    usedFallback = true;
  }

  const rawStdout = exec.rawStdout;
  const stderr = exec.stderr;
  const exitCode = exec.exitCode;
  let stdout = rawStdout;
  let sessionId = existing?.sessionId ?? "unknown";

  const rateLimitMessage = extractRateLimitMessage(rawStdout, stderr);
  if (rateLimitMessage) {
    stdout = rateLimitMessage;
  }

  // For new sessions, try to capture session ID if Qwen provides one
  if (!rateLimitMessage && isNew && exitCode === 0) {
    // Try to parse session ID from output or use a generated one
    sessionId = `qwenclaw-${Date.now()}`;
    await createSession(sessionId);
    console.log(`[${new Date().toLocaleTimeString()}] Session created: ${sessionId}`);
  }

  const result: RunResult = { stdout, stderr, exitCode };

  const output = [
    `# ${name}`,
    `Date: ${new Date().toISOString()}`,
    `Session: ${sessionId} (${isNew ? "new" : "resumed"})`,
    `Model config: ${usedFallback ? "fallback" : "primary"}`,
    `Prompt: ${prompt}`,
    `Exit code: ${result.exitCode}`,
    "",
    "## Output",
    stdout,
    ...(stderr ? ["## Stderr", stderr] : []),
  ].join("\n");

  await Bun.write(logFile, output);
  console.log(`[${new Date().toLocaleTimeString()}] Done: ${name} → ${logFile}`);

  return result;
}

export async function run(name: string, prompt: string): Promise<RunResult> {
  return enqueue(() => execQwen(name, prompt));
}

function prefixUserMessageWithClock(prompt: string): string {
  try {
    const settings = getSettings();
    const prefix = buildClockPromptPrefix(new Date(), settings.timezoneOffsetMinutes);
    return `${prefix}\n${prompt}`;
  } catch {
    const prefix = buildClockPromptPrefix(new Date(), 0);
    return `${prefix}\n${prompt}`;
  }
}

export async function runUserMessage(name: string, prompt: string): Promise<RunResult> {
  return run(name, prefixUserMessageWithClock(prompt));
}

/**
 * Bootstrap the session: fires Qwen with the system prompt so the
 * session is created immediately. No-op if a session already exists.
 */
export async function bootstrap(): Promise<void> {
  const existing = await getSession();
  if (existing) return;

  console.log(`[${new Date().toLocaleTimeString()}] Bootstrapping new session...`);
  await execQwen("bootstrap", "Wakeup, my friend!");
  console.log(`[${new Date().toLocaleTimeString()}] Bootstrap complete — session is live.`);
}
