import { join } from 'node:path';
import { createRouter } from '../../create-router';
import { AGENT_CONFIG_DIR } from '../../data-path';
import { invalidateModelCache } from '../chat/list-models';
import { logger } from '../chat/logger';
import { readConfigValue, writeConfigValue } from 'officerdb';

export const chatProvidersRouter = createRouter();

const AUTH_FILE = join(AGENT_CONFIG_DIR, 'auth.json');
const MODELS_FILE = join(AGENT_CONFIG_DIR, 'models.json');

// --- Local provider types ---

export type LocalProvider = {
  id: string;
  name: string;
  url: string;
  apiType: 'ollama' | 'openai-compatible' | 'lmstudio';
  auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string };
};

type ProbeResult = {
  success: boolean;
  apiType?: LocalProvider['apiType'];
  name?: string;
  needsAuth?: boolean;
  authType?: 'api-key' | 'basic' | 'unknown';
  models?: string[];
  error?: string;
};

type ModelConfig = {
  providers: Record<
    string,
    {
      baseUrl: string;
      apiKey?: string;
      api: string;
      models: {
        id: string;
        name: string;
        reasoning: boolean;
        input: string[];
        contextWindow: number;
        maxTokens: number;
        cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
      }[];
      _officer?: OfficerMeta;
    }
  >;
};

type OfficerMeta = {
  name: string;
  url: string;
  apiType: LocalProvider['apiType'];
  auth?: { type: 'api-key'; apiKey: string } | { type: 'basic'; username: string; password: string };
};

async function readModelsConfig(): Promise<ModelConfig> {
  try {
    const file = Bun.file(MODELS_FILE);
    if (!(await file.exists())) return { providers: {} };
    return (await file.json()) as ModelConfig;
  } catch {
    return { providers: {} };
  }
}

async function writeModelsConfig(config: ModelConfig): Promise<void> {
  await Bun.write(MODELS_FILE, JSON.stringify(config, null, 2));
}

/**
 * Read local providers from models.json by extracting officer-local-* entries.
 */
export async function readLocalProviders(): Promise<LocalProvider[]> {
  const config = await readModelsConfig();
  const providers: LocalProvider[] = [];
  for (const [key, entry] of Object.entries(config.providers)) {
    if (!key.startsWith('officer-local-') || !entry._officer) continue;
    const id = key.replace('officer-local-', '');
    providers.push({
      id,
      name: entry._officer.name,
      url: entry._officer.url,
      apiType: entry._officer.apiType,
      auth: entry._officer.auth,
    });
  }
  return providers;
}

async function probeUrl(url: string, auth?: LocalProvider['auth']): Promise<ProbeResult> {
  const base = url.replace(/\/+$/, '');
  const timeout = 5000;

  const headers: Record<string, string> = {};
  if (auth?.type === 'api-key') {
    headers['Authorization'] = `Bearer ${auth.apiKey}`;
  } else if (auth?.type === 'basic') {
    headers['Authorization'] = `Basic ${btoa(`${auth.username}:${auth.password}`)}`;
  }

  const tryFetch = async (path: string) => {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeout);
    try {
      const res = await fetch(`${base}${path}`, { headers, signal: controller.signal });
      return res;
    } catch {
      return null;
    } finally {
      clearTimeout(timer);
    }
  };

  // 1. Try Ollama: GET /api/tags
  const ollamaRes = await tryFetch('/api/tags');
  if (ollamaRes) {
    if (ollamaRes.status === 401 || ollamaRes.status === 403) {
      return { success: true, apiType: 'ollama', name: 'Ollama', needsAuth: true, authType: 'unknown' };
    }
    if (ollamaRes.ok) {
      try {
        const data = (await ollamaRes.json()) as { models?: { name: string }[] };
        if (data.models) {
          return {
            success: true,
            apiType: 'ollama',
            name: 'Ollama',
            needsAuth: false,
            models: data.models.map((m) => m.name),
          };
        }
      } catch {
        // not ollama, continue
      }
    }
  }

  // 2. Try LM Studio: GET /v1/models (LM Studio returns specific format)
  // 3. Try OpenAI-compatible: GET /v1/models
  const oaiRes = await tryFetch('/v1/models');
  if (oaiRes) {
    if (oaiRes.status === 401 || oaiRes.status === 403) {
      const wwwAuth = oaiRes.headers.get('www-authenticate') ?? '';
      const authType = wwwAuth.toLowerCase().includes('basic') ? ('basic' as const) : ('api-key' as const);
      return { success: true, apiType: 'openai-compatible', name: 'OpenAI-compatible', needsAuth: true, authType };
    }
    if (oaiRes.ok) {
      try {
        const data = (await oaiRes.json()) as { data?: { id: string }[]; object?: string };
        if (data.data) {
          // LM Studio includes "lm-studio" in model IDs
          const isLmStudio = data.data.some((m) => m.id.includes('lm-studio'));
          const apiType = isLmStudio ? ('lmstudio' as const) : ('openai-compatible' as const);
          const name = isLmStudio ? 'LM Studio' : 'OpenAI-compatible';
          return {
            success: true,
            apiType,
            name,
            needsAuth: false,
            models: data.data.map((m) => m.id),
          };
        }
      } catch {
        // not valid JSON
      }
    }
  }

  // 4. Try bare /models (some servers)
  const bareRes = await tryFetch('/models');
  if (bareRes) {
    if (bareRes.status === 401 || bareRes.status === 403) {
      return {
        success: true,
        apiType: 'openai-compatible',
        name: 'OpenAI-compatible',
        needsAuth: true,
        authType: 'api-key',
      };
    }
    if (bareRes.ok) {
      try {
        const data = (await bareRes.json()) as { data?: { id: string }[] };
        if (data.data) {
          return {
            success: true,
            apiType: 'openai-compatible',
            name: 'OpenAI-compatible',
            needsAuth: false,
            models: data.data.map((m) => m.id),
          };
        }
      } catch {
        // continue
      }
    }
  }

  return { success: false, error: 'Could not detect API type at this URL' };
}

// --- Model fetching for local providers ---

const REASONING_PATTERNS = [/\bqwen3\b/i, /\bqwq\b/i, /\bdeepseek-r1\b/i, /\br1\b/i, /\breasoning\b/i, /\bthink/i];

function isReasoningModel(modelId: string): boolean {
  return REASONING_PATTERNS.some((p) => p.test(modelId));
}

async function fetchModelsFromProvider(
  lp: LocalProvider,
): Promise<{ id: string; name?: string; contextWindow?: number; maxTokens?: number }[]> {
  try {
    const base = lp.url.replace(/\/+$/, '');
    const path = lp.apiType === 'ollama' ? '/api/tags' : '/v1/models';

    const headers: Record<string, string> = {};
    if (lp.auth?.type === 'api-key') headers['Authorization'] = `Bearer ${lp.auth.apiKey}`;
    else if (lp.auth?.type === 'basic')
      headers['Authorization'] = `Basic ${btoa(`${lp.auth.username}:${lp.auth.password}`)}`;

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 5000);
    const res = await fetch(`${base}${path}`, { headers, signal: controller.signal });
    clearTimeout(timer);

    if (!res.ok) return [];
    const data = await res.json();

    if (lp.apiType === 'ollama' && data.models) {
      return data.models.map((m: { name: string }) => ({
        id: m.name,
        name: m.name,
        contextWindow: 128000,
        maxTokens: 4096,
      }));
    } else if (data.data) {
      return data.data.map((m: { id: string }) => ({ id: m.id, name: m.id, contextWindow: 128000, maxTokens: 4096 }));
    }
    return [];
  } catch {
    return [];
  }
}

/**
 * Write a local provider entry directly into models.json with probed models.
 */
async function addLocalProviderToModelsConfig(lp: LocalProvider): Promise<void> {
  const models = await fetchModelsFromProvider(lp);
  if (models.length === 0) {
    logger.warn('No models found for local provider', { provider: lp.name });
  }

  const baseUrl = lp.apiType === 'ollama' ? `${lp.url}/v1` : lp.url.endsWith('/v1') ? lp.url : `${lp.url}/v1`;

  const config = await readModelsConfig();
  config.providers[`officer-local-${lp.id}`] = {
    baseUrl,
    apiKey: lp.auth?.type === 'api-key' ? lp.auth.apiKey : 'none',
    api: 'openai-completions',
    models: models.map((m) => ({
      id: m.id,
      name: m.name || m.id,
      reasoning: isReasoningModel(m.id),
      input: ['text'],
      contextWindow: m.contextWindow || 128000,
      maxTokens: m.maxTokens || 4096,
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    })),
    _officer: {
      name: lp.name,
      url: lp.url,
      apiType: lp.apiType,
      auth: lp.auth,
    },
  };
  await writeModelsConfig(config);
  invalidateModelCache();
}

async function removeLocalProviderFromModelsConfig(id: string): Promise<boolean> {
  const config = await readModelsConfig();
  const key = `officer-local-${id}`;
  if (!(key in config.providers)) return false;
  delete config.providers[key];
  await writeModelsConfig(config);
  invalidateModelCache();
  return true;
}

/**
 * Re-probe all local providers and update their models in models.json.
 */
async function refreshLocalProviders(): Promise<void> {
  const config = await readModelsConfig();
  for (const [key, entry] of Object.entries(config.providers)) {
    if (!key.startsWith('officer-local-') || !entry._officer) continue;
    const lp: LocalProvider = {
      id: key.replace('officer-local-', ''),
      name: entry._officer.name,
      url: entry._officer.url,
      apiType: entry._officer.apiType,
      auth: entry._officer.auth,
    };
    const models = await fetchModelsFromProvider(lp);

    const baseUrl = lp.apiType === 'ollama' ? `${lp.url}/v1` : lp.url.endsWith('/v1') ? lp.url : `${lp.url}/v1`;

    entry.baseUrl = baseUrl;
    entry.apiKey = lp.auth?.type === 'api-key' ? lp.auth.apiKey : 'none';
    entry.models = models.map((m) => ({
      id: m.id,
      name: m.name || m.id,
      reasoning: isReasoningModel(m.id),
      input: ['text'],
      contextWindow: m.contextWindow || 128000,
      maxTokens: m.maxTokens || 4096,
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    }));
  }
  await writeModelsConfig(config);
  invalidateModelCache();
}

type AuthEntry = { type: 'api_key'; key: string };
type AuthJson = Record<string, AuthEntry>;

async function readAuthJson(): Promise<AuthJson> {
  try {
    const file = Bun.file(AUTH_FILE);
    if (!(await file.exists())) return {};
    return (await file.json()) as AuthJson;
  } catch {
    return {};
  }
}

async function writeAuthJson(auth: AuthJson): Promise<void> {
  await Bun.write(AUTH_FILE, JSON.stringify(auth, null, 2));
}

/**
 * Hardcoded map of display name → Pi provider ID.
 * Pi's native providers have built-in models; adding a key to auth.json
 * is all that's needed to make their models appear in `pi --list-models`.
 */
export const PROVIDERS: { key: string; providerId: string }[] = [
  { key: 'Anthropic', providerId: 'anthropic' },
  { key: 'OpenAI', providerId: 'openai' },
  { key: 'Google', providerId: 'google' },
  { key: 'Groq', providerId: 'groq' },
  { key: 'Mistral', providerId: 'mistral' },
  { key: 'xAI', providerId: 'xai' },
  { key: 'OpenRouter', providerId: 'openrouter' },
  { key: 'MiniMax', providerId: 'minimax' },
  { key: 'Hugging Face', providerId: 'huggingface' },
  { key: 'Azure OpenAI', providerId: 'azure-openai-responses' },
  { key: 'OpenCode Zen', providerId: 'opencode' },
  { key: 'ZAI', providerId: 'zai' },
  { key: 'Cerebras', providerId: 'cerebras' },
];

chatProvidersRouter.get('/providers', async (ctx) => {
  return ctx.json(PROVIDERS);
});

// GET /auth reported which providers had a key set. No caller — AIHarnessesSection reads /api-keys and
// /api-keys/health instead, which say the same thing with more detail.

const maskValue = (value: string) => {
  if (value.length <= 8) return '***';
  return value.slice(0, 3) + '...' + value.slice(-3);
};

chatProvidersRouter.get('/api-keys', async (ctx) => {
  const auth = await readAuthJson();
  const keys = PROVIDERS.filter((p) => auth[p.providerId]?.key?.trim()).map((p) => ({
    provider: p.providerId,
    value: maskValue(auth[p.providerId]!.key),
  }));
  return ctx.json({ keys });
});

chatProvidersRouter.put('/api-keys', async (ctx) => {
  const { provider, value } = await ctx.req.json<{ provider: string; value: string }>();
  if (!PROVIDERS.some((p) => p.providerId === provider)) return ctx.json({ error: 'Invalid provider' }, 400);

  const auth = await readAuthJson();
  if (value.trim()) {
    auth[provider] = { type: 'api_key', key: value.trim() };
  } else {
    delete auth[provider];
  }
  await writeAuthJson(auth);
  invalidateModelCache();
  return ctx.json({ ok: true });
});

type AccessPolicy = { allowedModels: string[] };
const ACCESS_POLICY_KEY = 'chat-access-policy';

chatProvidersRouter.get('/access-policy', async (ctx) => {
  const policy = await readConfigValue<AccessPolicy>(ACCESS_POLICY_KEY, { allowedModels: [] });
  return ctx.json(policy);
});

chatProvidersRouter.put('/access-policy', async (ctx) => {
  const body = await ctx.req.json<AccessPolicy>();
  await writeConfigValue(ACCESS_POLICY_KEY, body);
  return ctx.json(body);
});

const REMOTE_HEALTH_CONFIG: Record<
  string,
  {
    url: string | ((key: string) => string);
    headers: (key: string) => Record<string, string>;
  }
> = {
  openai: { url: 'https://api.openai.com/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
  anthropic: {
    url: 'https://api.anthropic.com/v1/models',
    headers: (k) => ({ 'x-api-key': k, 'anthropic-version': '2023-06-01' }),
  },
  google: { url: (k) => `https://generativelanguage.googleapis.com/v1beta/models?key=${k}`, headers: () => ({}) },
  groq: { url: 'https://api.groq.com/openai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
  mistral: { url: 'https://api.mistral.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
  xai: { url: 'https://api.x.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
  openrouter: { url: 'https://openrouter.ai/api/v1/auth/key', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
  cerebras: { url: 'https://api.cerebras.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
  opencode: { url: 'https://opencode.ai/zen/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
  zai: { url: 'https://opencode.ai/v1/models', headers: (k) => ({ Authorization: `Bearer ${k}` }) },
};

chatProvidersRouter.get('/api-keys/health', async (ctx) => {
  const auth = await readAuthJson();
  const results: Record<string, boolean | null> = {};

  const checks = PROVIDERS.filter((p) => auth[p.providerId]?.key?.trim()).map(async (p) => {
    const config = REMOTE_HEALTH_CONFIG[p.providerId];
    if (!config) {
      results[p.providerId] = null;
      return;
    }
    const key = auth[p.providerId]!.key;
    const url = typeof config.url === 'function' ? config.url(key) : config.url;
    const headers = config.headers(key);
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 3000);
    try {
      const res = await fetch(url, { headers, signal: controller.signal });
      results[p.providerId] = res.ok;
    } catch {
      results[p.providerId] = false;
    } finally {
      clearTimeout(timer);
    }
  });

  await Promise.all(checks);
  return ctx.json(results);
});

/**
 * Resolve the full path to the `pi` binary.
 * Checks PATH first, then falls back to the npm global bin directory
 * (which may not be in PATH when the server is managed by pm2).
 */
/**
 * Finds the Pi package directory (containing package.json).
 * Checks: PATH → npm global prefix → ~/.npm-global fallback.
 */
async function resolvePiPackageDir(): Promise<string | null> {
  const candidates: string[] = [];

  // 1. Try PATH
  try {
    const proc = Bun.spawn(['which', 'pi'], { stdout: 'pipe', stderr: 'pipe' });
    const output = (await new Response(proc.stdout).text()).trim();
    await proc.exited;
    if (proc.exitCode === 0 && output) {
      // Resolve symlink: bin/pi -> ../lib/node_modules/.../dist/cli.js
      const resolved = (await Bun.file(output).exists()) ? output : null;
      if (resolved) {
        // Walk up from bin to find the package
        const npmGlobalLib = join(output, '..', '..', 'lib', 'node_modules', '@mariozechner', 'pi-coding-agent');
        candidates.push(npmGlobalLib);
      }
    }
  } catch {}

  // 2. Common locations
  const home = process.env.HOME ?? '';
  candidates.push(
    join(home, '.npm-global', 'lib', 'node_modules', '@mariozechner', 'pi-coding-agent'),
    '/usr/local/lib/node_modules/@mariozechner/pi-coding-agent',
  );

  for (const dir of candidates) {
    const pkgFile = join(dir, 'package.json');
    if (await Bun.file(pkgFile).exists()) return dir;
  }
  return null;
}

/** Read Pi version directly from its package.json — avoids shebang/spawn issues. */
async function getPiVersion(): Promise<{ version: string | null; path: string | null }> {
  const dir = await resolvePiPackageDir();
  if (!dir) return { version: null, path: null };
  try {
    const pkg = await Bun.file(join(dir, 'package.json')).json();
    return { version: pkg.version ?? null, path: dir };
  } catch {
    return { version: null, path: dir };
  }
}

chatProvidersRouter.get('/version', async (ctx) => {
  const { version, path } = await getPiVersion();
  return ctx.json({ version, path, globalPath: path });
});

chatProvidersRouter.post('/install', async (ctx) => {
  try {
    const proc = Bun.spawn(['npm', 'install', '-g', '@mariozechner/pi-coding-agent'], {
      stdout: 'pipe',
      stderr: 'pipe',
    });
    const stderr = await new Response(proc.stderr).text();
    await proc.exited;
    if (proc.exitCode !== 0) {
      return ctx.json({ version: null, path: null, globalPath: null, error: stderr.trim() }, 500);
    }

    const { version, path } = await getPiVersion();
    if (!version)
      return ctx.json({ version: null, path, globalPath: path, error: 'Installed but package.json not found' }, 500);
    return ctx.json({ version, path, globalPath: path });
  } catch {
    return ctx.json({ version: null, path: null, globalPath: null, error: 'Installation failed' }, 500);
  }
});

// --- Local providers ---

chatProvidersRouter.get('/local-providers', async (ctx) => {
  const providers = await readLocalProviders();
  return ctx.json(
    providers.map((p) => ({
      ...p,
      auth: p.auth ? { type: p.auth.type } : undefined,
    })),
  );
});

chatProvidersRouter.post('/local-providers/probe', async (ctx) => {
  const { url, auth } = await ctx.req.json<{ url: string; auth?: LocalProvider['auth'] }>();
  if (!url?.trim()) return ctx.json({ success: false, error: 'URL is required' }, 400);
  const result = await probeUrl(url.trim(), auth);
  return ctx.json(result);
});

chatProvidersRouter.post('/local-providers', async (ctx) => {
  const body = await ctx.req.json<{
    url: string;
    name?: string;
    apiType: LocalProvider['apiType'];
    auth?: LocalProvider['auth'];
  }>();

  const provider: LocalProvider = {
    id: crypto.randomUUID(),
    name: body.name ?? body.apiType,
    url: body.url.replace(/\/+$/, ''),
    apiType: body.apiType,
    auth: body.auth,
  };

  await addLocalProviderToModelsConfig(provider);
  return ctx.json({ ...provider, auth: provider.auth ? { type: provider.auth.type } : undefined });
});

chatProvidersRouter.delete('/local-providers/:id', async (ctx) => {
  const { id } = ctx.req.param();
  const removed = await removeLocalProviderFromModelsConfig(id);
  if (!removed) return ctx.json({ error: 'Not found' }, 404);
  return ctx.json({ ok: true });
});

chatProvidersRouter.post('/local-providers/refresh', async (ctx) => {
  await refreshLocalProviders();
  return ctx.json({ ok: true });
});

chatProvidersRouter.get('/local-providers/health', async (ctx) => {
  const providers = await readLocalProviders();
  const results: Record<string, boolean> = {};

  await Promise.all(
    providers.map(async (p) => {
      const base = p.url.replace(/\/+$/, '');
      const path = p.apiType === 'ollama' ? '/api/tags' : '/v1/models';
      const headers: Record<string, string> = {};
      if (p.auth?.type === 'api-key') headers['Authorization'] = `Bearer ${p.auth.apiKey}`;
      else if (p.auth?.type === 'basic')
        headers['Authorization'] = `Basic ${btoa(`${p.auth.username}:${p.auth.password}`)}`;

      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), 3000);
      try {
        const res = await fetch(`${base}${path}`, { headers, signal: controller.signal });
        results[p.id] = res.ok;
      } catch {
        results[p.id] = false;
      } finally {
        clearTimeout(timer);
      }
    }),
  );

  return ctx.json(results);
});
