import express from 'express';
import fsPromises from 'fs/promises';
import path from 'path';
import { spawn } from 'child_process';
import { parse as parseYaml } from 'yaml';
import {
  buildDefaultPilotDeckConfig,
  configToYaml,
  getPilotDeckConfigPath,
  maskSecrets,
  parseConfigYaml,
  preserveMaskedSecrets,
  rawYamlToMaskedString,
  readPilotDeckConfigFile,
  validatePilotDeckConfig,
  writePilotDeckConfig,
  writeRawPilotDeckYaml,
} from '../services/pilotdeckConfig.js';
import { reloadPilotDeckConfig } from '../services/pilotdeckConfigReloader.js';
import { suppressNextWatchEvent } from '../services/pilotdeckConfigWatcher.js';
import { getPilotDeckGateway } from '../pilotdeck-bridge.js';

async function notifyGatewayConfigReload() {
  try {
    const gw = await getPilotDeckGateway();
    if (gw?.reloadConfig) await gw.reloadConfig();
  } catch { /* gateway unreachable — self-watch will pick up the change */ }
}

const router = express.Router();

function serializeConfigResponse(record, reloadResult = null) {
  const validation = validatePilotDeckConfig(record.config);
  const maskedConfig = maskSecrets(record.config);
  // Prefer the disk's actual YAML for the "raw" view so non-ui-internal
  // top-level segments (router/gateway/adapters/extension/cron/alwaysOn)
  // survive the trip from disk → UI. Fall back to the lossy template
  // only when there's no disk file yet (fresh install), so the editor
  // still has something editable to render.
  const hasDiskYaml = record.rawYaml && typeof record.rawYaml === 'object' && Object.keys(record.rawYaml).length > 0;
  const raw = hasDiskYaml ? rawYamlToMaskedString(record.rawYaml) : configToYaml(maskedConfig);
  return {
    exists: record.exists,
    path: record.configPath,
    raw,
    config: maskedConfig,
    validation: {
      valid: validation.valid,
      errors: validation.errors,
      warnings: validation.warnings,
    },
    ...(reloadResult ? { reload: reloadResult } : {}),
  };
}

function broadcastConfigEvent(payload) {
  process.emit('pilotdeck:config-broadcast', payload);
}

router.get('/', (_req, res) => {
  try {
    const record = readPilotDeckConfigFile();
    res.json(serializeConfigResponse(record));
  } catch (error) {
    res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
  }
});

router.post('/validate', (req, res) => {
  try {
    const raw = typeof req.body?.raw === 'string' ? req.body.raw : '';
    const config = raw ? parseConfigYaml(raw) : req.body?.config;
    const validation = validatePilotDeckConfig(config);
    res.status(validation.valid ? 200 : 400).json(validation);
  } catch (error) {
    res.status(400).json({ valid: false, errors: [error instanceof Error ? error.message : String(error)], warnings: [] });
  }
});

router.put('/', async (req, res) => {
  try {
    // Two submission shapes coexist:
    //
    //   • `{ raw: "..." }` from the Raw YAML editor → write the
    //     parsed YAML object to disk verbatim via
    //     writeRawPilotDeckYaml. This is the only path that preserves
    //     router/gateway/adapters/extension/cron/alwaysOn edits,
    //     because the ui-internal schema doesn't model them.
    //
    //   • `{ config: {...} }` from structured editors (provider
    //     picker, memory editor, onboarding LLM step) → run through
    //     writePilotDeckConfig, which round-trips through
    //     ui-internal but read-modify-writes the rest from disk so
    //     non-ui segments aren't dropped.
    //
    // Removing the `config` branch is what got 5ad9f29 reverted;
    // never collapse the two paths into one — they have different
    // semantics and different callers.
    const diskRecord = readPilotDeckConfigFile();
    const rawString = typeof req.body?.raw === 'string' ? req.body.raw : null;

    let saved;
    if (rawString !== null) {
      let parsed;
      try {
        parsed = parseYaml(rawString);
      } catch (parseErr) {
        return res.status(400).json({
          error: `Invalid YAML: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
        });
      }
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        return res.status(400).json({ error: 'raw YAML must parse to an object' });
      }
      // Re-hydrate any field the UI received as "********" with the
      // original disk value so saving the masked view back is a no-op
      // for secrets the user didn't actually touch.
      const restored = preserveMaskedSecrets(parsed, diskRecord.rawYaml ?? {});
      suppressNextWatchEvent();
      saved = await writeRawPilotDeckYaml(restored);
    } else if (req.body?.config && typeof req.body.config === 'object') {
      const restored = preserveMaskedSecrets(req.body.config, diskRecord.config);
      suppressNextWatchEvent();
      saved = await writePilotDeckConfig(restored);
    } else {
      return res.status(400).json({ error: 'raw YAML or config object is required' });
    }

    const reloadResult = await reloadPilotDeckConfig(saved.config);
    void notifyGatewayConfigReload();
    // Re-read disk so the response's `raw` field comes from the actual
    // (lossless) file rather than the lossy round-trip output, and so
    // `serializeConfigResponse` has a `rawYaml` to render the full view.
    const freshRecord = readPilotDeckConfigFile();
    const response = serializeConfigResponse(freshRecord, reloadResult);
    broadcastConfigEvent({ source: 'ui-save', ...response, timestamp: new Date().toISOString() });
    res.json(response);
  } catch (error) {
    if (error?.validation) {
      return res.status(400).json({ error: error.message, validation: error.validation });
    }
    res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
  }
});

router.post('/reload', async (_req, res) => {
  try {
    const record = readPilotDeckConfigFile();
    const validation = validatePilotDeckConfig(record.config);
    if (!validation.valid) {
      return res.status(400).json({ error: 'Invalid config', validation });
    }
    const reloadResult = await reloadPilotDeckConfig(record.config);
    void notifyGatewayConfigReload();
    const response = serializeConfigResponse(record, reloadResult);
    broadcastConfigEvent({ source: 'ui-reload', ...response, timestamp: new Date().toISOString() });
    res.json(response);
  } catch (error) {
    res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
  }
});

router.get('/provider', (_req, res) => {
  try {
    const record = readPilotDeckConfigFile();
    const providers = record.config?.model?.providers;
    if (!providers || typeof providers !== 'object') {
      return res.json({ exists: false, provider: null });
    }

    const mainRef = typeof record.config?.agent?.model === 'string'
      ? record.config.agent.model.trim()
      : '';
    let providerId = '';
    let modelId = '';
    if (mainRef) {
      const slash = mainRef.indexOf('/');
      if (slash > 0 && slash < mainRef.length - 1) {
        providerId = mainRef.slice(0, slash);
        modelId = mainRef.slice(slash + 1);
      }
    }
    if (!providerId) {
      providerId = Object.keys(providers)[0] || '';
      if (providerId) {
        const firstModels = providers[providerId]?.models;
        modelId = firstModels && typeof firstModels === 'object'
          ? (Object.keys(firstModels)[0] || '')
          : '';
      }
    }
    if (!providerId) return res.json({ exists: false, provider: null });

    const provider = providers[providerId] || {};

    res.json({
      exists: true,
      provider: {
        type: provider.protocol || '',
        baseUrl: provider.url || '',
        apiKey: provider.apiKey || '',
        model: modelId,
      },
    });
  } catch (error) {
    res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
  }
});

router.post('/test-connection', async (req, res) => {
  const { providerType, baseUrl, apiKey, model } = req.body || {};
  if (!baseUrl || !apiKey || !model) {
    return res.status(400).json({ ok: false, error: 'baseUrl, apiKey, and model are required' });
  }

  // Accept V2 protocols ('openai' | 'anthropic') as well as the legacy
  // onboarding values ('openai-chat' | 'anthropic') for compatibility.
  const normalizedType = String(providerType || '').toLowerCase();
  const isAnthropic = normalizedType === 'anthropic';
  const normalizedBaseUrl = String(baseUrl).trim().replace(/\/+$/, '');
  const timeout = 10_000;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  try {
    let url;
    let fetchOptions;

    if (isAnthropic) {
      url = `${normalizedBaseUrl}/v1/messages`;
      fetchOptions = {
        method: 'POST',
        headers: {
          'x-api-key': apiKey,
          'anthropic-version': '2023-06-01',
          'content-type': 'application/json',
        },
        body: JSON.stringify({
          model,
          max_tokens: 1,
          messages: [{ role: 'user', content: 'Hi' }],
        }),
        signal: controller.signal,
      };
    } else {
      url = `${normalizedBaseUrl}/chat/completions`;
      fetchOptions = {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'content-type': 'application/json',
        },
        body: JSON.stringify({
          model,
          max_tokens: 1,
          messages: [{ role: 'user', content: 'Hi' }],
        }),
        signal: controller.signal,
      };
    }

    const response = await fetch(url, fetchOptions);
    clearTimeout(timer);
    const responseText = await response.text();

    if (response.ok) {
      let body;
      try {
        body = JSON.parse(responseText);
      } catch {
        return res.json({
          ok: false,
          error: `Expected a JSON completion response but received non-JSON content from ${url}. For OpenAI-compatible endpoints, the base URL usually ends with /v1.`,
        });
      }

      const hasCompletionShape = isAnthropic
        ? Array.isArray(body?.content) || body?.type === 'message'
        : Array.isArray(body?.choices);
      if (!hasCompletionShape) {
        return res.json({
          ok: false,
          error: `Endpoint returned HTTP ${response.status}, but the response was not a valid ${isAnthropic ? 'Anthropic message' : 'OpenAI chat completion'}. Check the base URL path.`,
        });
      }

      return res.json({ ok: true, message: `Connected successfully — Model ${model} is available.` });
    }

    let detail = `${response.status} ${response.statusText}`;
    try {
      const body = JSON.parse(responseText);
      if (body?.error?.message) detail = body.error.message;
      else if (body?.error?.type) detail = `${body.error.type}: ${body.error.message || ''}`;
    } catch { /* ignore parse errors */ }

    return res.json({ ok: false, error: `${detail}` });
  } catch (err) {
    clearTimeout(timer);
    if (err.name === 'AbortError') {
      return res.json({ ok: false, error: `Connection timed out after ${timeout / 1000}s. Check your network and API URL.` });
    }
    return res.json({ ok: false, error: err.message || String(err) });
  }
});

/**
 * Probe the configured web-search provider. Mirrors
 * `src/tool/builtin/webSearch.ts`'s GLM/Tavily/custom request shape. Returns:
 * `{ ok, error?, latencyMs?, organicCount? }` to match the convention
 * established by `/test-connection`.
 */
router.post('/test-web-search', async (req, res) => {
  const { provider, apiKey, endpoint, customProvider } = req.body || {};
  const selectedProvider = provider === 'tavily' || provider === 'custom' ? provider : 'glm';
  const custom = customProvider && typeof customProvider === 'object' ? customProvider : {};
  const customAuth = typeof custom.auth === 'string' ? custom.auth : 'bearer';
  const customMethod = custom.method === 'GET' ? 'GET' : 'POST';
  const queryParam = typeof custom.queryParam === 'string' && custom.queryParam.trim() ? custom.queryParam.trim() : 'query';
  const apiKeyParam = typeof custom.apiKeyParam === 'string' && custom.apiKeyParam.trim() ? custom.apiKeyParam.trim() : 'api_key';
  const resultsPath = typeof custom.resultsPath === 'string' ? custom.resultsPath.trim() : '';
  const trimmedKey = typeof apiKey === 'string' ? apiKey.trim() : '';
  if (!trimmedKey && !(selectedProvider === 'custom' && customAuth === 'none')) {
    return res.status(400).json({ ok: false, error: 'API key is required.' });
  }
  const trimmedEndpoint = typeof endpoint === 'string' ? endpoint.trim() : '';
  if (selectedProvider === 'custom' && !trimmedEndpoint) {
    return res.status(400).json({ ok: false, error: 'Custom provider endpoint is required.' });
  }
  const effectiveEndpoint = trimmedEndpoint || (
    selectedProvider === 'tavily'
      ? 'https://api.tavily.com/search'
      : 'https://api.z.ai/api/paas/v4/web_search'
  );

  let requestUrl;
  let requestInit;
  try {
    const url = new URL(effectiveEndpoint);
    if (selectedProvider === 'tavily') {
      requestUrl = effectiveEndpoint;
      requestInit = {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
          },
          body: JSON.stringify({
            api_key: trimmedKey,
            query: 'hello',
            max_results: 3,
            include_answer: true,
            search_depth: 'basic',
          }),
        };
    } else if (selectedProvider === 'custom') {
      const headers = { Accept: 'application/json' };
      const body = {};
      if (customMethod === 'GET') {
        url.searchParams.set(queryParam, 'hello');
      } else {
        headers['Content-Type'] = 'application/json';
        body[queryParam] = 'hello';
      }
      if (customAuth === 'bearer' && trimmedKey) {
        headers.Authorization = `Bearer ${trimmedKey}`;
      } else if (customAuth === 'queryApiKey' && trimmedKey) {
        url.searchParams.set(apiKeyParam, trimmedKey);
      } else if (customAuth === 'bodyApiKey' && trimmedKey) {
        if (customMethod === 'GET') url.searchParams.set(apiKeyParam, trimmedKey);
        else body[apiKeyParam] = trimmedKey;
      }
      requestUrl = url.toString();
      requestInit = {
        method: customMethod,
        headers,
        ...(customMethod === 'POST' ? { body: JSON.stringify(body) } : {}),
      };
    } else {
      requestUrl = effectiveEndpoint;
      requestInit = {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${trimmedKey}`,
            'Content-Type': 'application/json',
            Accept: 'application/json',
          },
          body: JSON.stringify({
            search_engine: 'search-prime',
            search_query: 'hello',
            count: 3,
            search_recency_filter: 'noLimit',
          }),
        };
    }
  } catch {
    return res.status(400).json({ ok: false, error: `Invalid endpoint URL: ${effectiveEndpoint}` });
  }

  const timeout = 15_000;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);
  const t0 = Date.now();

  try {
    const response = await fetch(requestUrl, { ...requestInit, signal: controller.signal });
    clearTimeout(timer);
    const latencyMs = Date.now() - t0;

    let raw = null;
    try {
      raw = await response.json();
    } catch { /* not JSON */ }

    if (!response.ok) {
      const detail = (raw && (raw.error || raw.msg)) || `${response.status} ${response.statusText}`;
      return res.json({ ok: false, error: String(detail), latencyMs });
    }
    if (raw && typeof raw.error === 'string' && raw.error.length > 0) {
      return res.json({ ok: false, error: raw.error, latencyMs });
    }
    if (raw && typeof raw.code === 'number' && raw.code !== 0) {
      const msg = typeof raw.msg === 'string' ? raw.msg : 'proxy error';
      return res.json({ ok: false, error: `code=${raw.code}: ${msg}`, latencyMs });
    }

    const organic = selectedProvider === 'tavily'
      ? raw?.results
      : selectedProvider === 'custom' && resultsPath
        ? readPath(raw, resultsPath)
        : (raw?.search_result ?? raw?.results ?? raw?.items ?? raw?.data);
    const organicCount = Array.isArray(organic) ? organic.length : 0;
    return res.json({ ok: true, latencyMs, organicCount });
  } catch (err) {
    clearTimeout(timer);
    if (err.name === 'AbortError') {
      return res.json({ ok: false, error: `Connection timed out after ${timeout / 1000}s.` });
    }
    return res.json({ ok: false, error: err.message || String(err) });
  }
});

function readPath(value, pathValue) {
  return pathValue.split('.').reduce((current, segment) => {
    if (!current || typeof current !== 'object' || Array.isArray(current)) return undefined;
    return current[segment];
  }, value);
}

router.post('/open', async (_req, res) => {
  const configPath = getPilotDeckConfigPath();
  try {
    await fsPromises.mkdir(path.dirname(configPath), { recursive: true });
    try {
      await fsPromises.access(configPath);
    } catch {
      await fsPromises.writeFile(configPath, configToYaml(buildDefaultPilotDeckConfig()), 'utf8');
    }

    const command = process.platform === 'darwin'
      ? 'open'
      : process.platform === 'win32'
        ? 'cmd'
        : 'xdg-open';
    const args = process.platform === 'darwin'
      ? ['-R', configPath]
      : process.platform === 'win32'
        ? ['/c', 'start', '', configPath]
        : [path.dirname(configPath)];
    const child = spawn(command, args, { stdio: 'ignore', detached: true });
    child.unref();
    res.json({ success: true, path: configPath });
  } catch (error) {
    res.json({ success: false, path: configPath, error: error instanceof Error ? error.message : String(error) });
  }
});

export default router;
