import type { Command } from 'commander';
import type { Config } from '../config/index.js';
import { getConfigPath, getDataDir, formatStatus, resolveOverlayPath } from './shared.js';
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
import { homedir } from 'os';
import { resolve, join } from 'path';
import { parse } from 'yaml';
import { configSchema } from '../config/schema.js';

export interface CheckResult {
  status: 'pass' | 'fail' | 'warn' | 'skip';
  label: string;
  detail?: string;
}

export interface DoctorContext {
  configPath: string;
  dataDir: string;
  config?: Config;
}

type Check = (ctx: DoctorContext) => Promise<CheckResult>;

const checkConfigExists: Check = async (ctx) => {
  if (existsSync(ctx.configPath)) {
    return { status: 'pass', label: 'Config file exists', detail: `(${ctx.configPath})` };
  }
  return { status: 'fail', label: 'Config file exists', detail: `not found at ${ctx.configPath}` };
};

const checkOverlayExists: Check = async (ctx) => {
  const overlayPath = resolveOverlayPath(ctx.configPath);
  if (!overlayPath) {
    return { status: 'skip', label: 'Config overlay', detail: '(FLYNN_ENV not set)' };
  }
  const env = process.env.FLYNN_ENV;
  if (existsSync(overlayPath)) {
    return { status: 'pass', label: 'Config overlay', detail: `(${env}.yaml found)` };
  }
  return { status: 'fail', label: 'Config overlay', detail: `FLYNN_ENV=${env} but ${overlayPath} not found` };
};

const checkConfigParses: Check = async (ctx) => {
  if (!existsSync(ctx.configPath)) {
    return { status: 'skip', label: 'Config parses', detail: '(no config file)' };
  }
  try {
    const raw = readFileSync(ctx.configPath, 'utf-8');
    parse(raw);
    return { status: 'pass', label: 'Config parses', detail: '(valid YAML)' };
  } catch (err) {
    return { status: 'fail', label: 'Config parses', detail: err instanceof Error ? err.message : String(err) };
  }
};

const checkConfigValidates: Check = async (ctx) => {
  if (!existsSync(ctx.configPath)) {
    return { status: 'skip', label: 'Config validates', detail: '(no config file)' };
  }
  try {
    const raw = readFileSync(ctx.configPath, 'utf-8');
    const parsed = parse(raw);
    if (!parsed || typeof parsed !== 'object') {
      return { status: 'fail', label: 'Config validates', detail: 'YAML did not produce an object' };
    }
    const config = configSchema.parse(parsed);
    ctx.config = config;
    return { status: 'pass', label: 'Config validates', detail: '(schema valid)' };
  } catch (err) {
    return { status: 'fail', label: 'Config validates', detail: err instanceof Error ? err.message : String(err) };
  }
};

const checkDeprecatedConfigKeys: Check = async (ctx) => {
  if (!existsSync(ctx.configPath)) {
    return { status: 'skip', label: 'Config deprecated keys', detail: '(no config file)' };
  }

  try {
    const raw = readFileSync(ctx.configPath, 'utf-8');
    const parsed = parse(raw) as any;
    const tailscaleOnly = Boolean(parsed?.server && typeof parsed.server === 'object' && 'tailscale_only' in parsed.server);

    if (tailscaleOnly) {
      return {
        status: 'warn',
        label: 'Config deprecated keys',
        detail: 'server.tailscale_only is deprecated/ignored; use server.tailscale.* + server.localhost instead',
      };
    }

    return { status: 'pass', label: 'Config deprecated keys' };
  } catch {
    return { status: 'skip', label: 'Config deprecated keys', detail: '(could not read/parse config)' };
  }
};

const checkEnvVars: Check = async (ctx) => {
  if (!existsSync(ctx.configPath)) {
    return { status: 'skip', label: 'Env vars resolved', detail: '(no config file)' };
  }
  try {
    const raw = readFileSync(ctx.configPath, 'utf-8');
    const envVarPattern = /\$\{([^}]+)\}/g;
    const unresolved: string[] = [];
    let match;
    while ((match = envVarPattern.exec(raw)) !== null) {
      if (!process.env[match[1]]) {
        unresolved.push(match[1]);
      }
    }
    if (unresolved.length > 0) {
      return { status: 'fail', label: 'Env vars resolved', detail: `unset: ${unresolved.join(', ')}` };
    }
    return { status: 'pass', label: 'Env vars resolved' };
  } catch {
    return { status: 'skip', label: 'Env vars resolved', detail: '(could not read config)' };
  }
};

const checkDataDir: Check = async (ctx) => {
  try {
    const { mkdirSync } = await import('fs');
    mkdirSync(ctx.dataDir, { recursive: true });

    // Test write access
    const testFile = join(ctx.dataDir, '.doctor-check');
    writeFileSync(testFile, 'ok');
    unlinkSync(testFile);
    return { status: 'pass', label: 'Data directory writable', detail: `(${ctx.dataDir})` };
  } catch {
    return { status: 'fail', label: 'Data directory writable', detail: `cannot write to ${ctx.dataDir}` };
  }
};

const checkSessionDb: Check = async (ctx) => {
  try {
    const { mkdirSync } = await import('fs');
    mkdirSync(ctx.dataDir, { recursive: true });

    const dbPath = resolve(ctx.dataDir, 'sessions.db');
    const { SessionStore } = await import('../session/index.js');
    const store = new SessionStore(dbPath);
    store.listSessions(); // Quick query to verify DB works
    store.close();
    return { status: 'pass', label: 'Session DB accessible', detail: '(sessions.db)' };
  } catch (err) {
    return { status: 'fail', label: 'Session DB accessible', detail: err instanceof Error ? err.message : String(err) };
  }
};

const checkModelConnectivity: Check = async (ctx) => {
  if (!ctx.config) {
    return { status: 'skip', label: 'Model connectivity', detail: '(config invalid)' };
  }
  const models = ctx.config.models;
  const model = models.default;
  if (!model.model) {
    return { status: 'fail', label: 'Model connectivity', detail: 'no default model configured' };
  }

  type AuthMode = 'auto' | 'api_key' | 'oauth';

  const getEffectiveAuthMode = (cfg: Record<string, unknown>): AuthMode => {
    const mode = cfg.auth_mode;
    if (mode === 'auto' || mode === 'api_key' || mode === 'oauth') {
      return mode;
    }
    const useOauth = Boolean(cfg.use_oauth);
    if (useOauth) {
      return 'oauth';
    }
    return 'auto';
  };

  const loadAuthStore = (): Record<string, unknown> => {
    const home = process.env.HOME ?? homedir();
    const authFile = resolve(home, '.config/flynn/auth.json');
    try {
      const raw = readFileSync(authFile, 'utf-8');
      const parsed = JSON.parse(raw) as unknown;
      return (parsed && typeof parsed === 'object') ? parsed as Record<string, unknown> : {};
    } catch {
      return {};
    }
  };

  const store = loadAuthStore();

  const storeOpenAIOAuthPresent = (): boolean => {
    const openai = store.openai as unknown;
    if (!openai || typeof openai !== 'object') {
      return false;
    }
    const o = openai as Record<string, unknown>;
    // Legacy: auth.json.openai = { access_token, refresh_token, ... }
    if (typeof o.access_token === 'string' && typeof o.refresh_token === 'string') {
      return true;
    }
    const oauth = o.oauth;
    return Boolean(
      oauth
      && typeof oauth === 'object'
      && typeof (oauth as any).access_token === 'string'
      && typeof (oauth as any).refresh_token === 'string',
    );
  };

  const storeOpenAIApiKeyPresent = (): boolean => {
    const openai = store.openai as unknown;
    if (!openai || typeof openai !== 'object') {
      return false;
    }
    const o = openai as Record<string, unknown>;
    const apiKey = o.api_key;
    return Boolean(
      apiKey
      && typeof apiKey === 'object'
      && typeof (apiKey as any).api_key === 'string'
      && (apiKey as any).api_key.length > 0,
    );
  };

  const storeAnthropicApiKeyPresent = (): boolean => {
    const anthropic = store.anthropic as any;
    return Boolean(anthropic && typeof anthropic.api_key === 'string' && anthropic.api_key.length > 0);
  };

  const storeAnthropicAuthTokenPresent = (): boolean => {
    const anthropic = store.anthropic as any;
    return Boolean(anthropic && typeof anthropic.auth_token === 'string' && anthropic.auth_token.length > 0);
  };

  const formatSources = (sources: { config: boolean; env: boolean; store: boolean }): string => {
    const parts: string[] = [];
    if (sources.config) {parts.push('config');}
    if (sources.env) {parts.push('env');}
    if (sources.store) {parts.push('store');}
    return parts.length > 0 ? parts.join('+') : 'missing';
  };

  const checkTierAuth = (tier: string, cfg: Record<string, unknown>): { status: 'pass' | 'warn' | 'fail'; detail?: string } => {
    const provider = String(cfg.provider ?? '');
    const modelName = String(cfg.model ?? '');
    const authMode = getEffectiveAuthMode(cfg);

    if (provider === 'openai') {
      if (authMode === 'oauth') {
        const ok = storeOpenAIOAuthPresent();
        if (!ok) {
          const status = tier === 'default' ? 'fail' : 'warn';
          return { status, detail: `${tier}: openai/${modelName} (auth_mode=oauth, oauth=missing — run flynn openai-auth)` };
        }
        return { status: 'pass', detail: `${tier}: openai/${modelName} (auth_mode=oauth, oauth=store)` };
      }

      const sources = {
        config: typeof cfg.api_key === 'string' && (cfg.api_key as string).length > 0,
        env: typeof process.env.OPENAI_API_KEY === 'string' && process.env.OPENAI_API_KEY.length > 0,
        store: storeOpenAIApiKeyPresent(),
      };

      const ok = sources.config || sources.env || sources.store;
      if (!ok) {
        const status = (authMode === 'api_key' && tier === 'default') ? 'fail' : 'warn';
        const hint = authMode === 'api_key'
          ? 'Set OPENAI_API_KEY or run flynn openai-key'
          : 'Set OPENAI_API_KEY (or run flynn openai-key) or run flynn openai-auth';
        return { status, detail: `${tier}: openai/${modelName} (auth_mode=${authMode}, api_key=${formatSources(sources)} — ${hint})` };
      }
      return { status: 'pass', detail: `${tier}: openai/${modelName} (auth_mode=${authMode}, api_key=${formatSources(sources)})` };
    }

    if (provider === 'anthropic') {
      if (authMode === 'oauth') {
        const sources = {
          config: typeof cfg.auth_token === 'string' && (cfg.auth_token as string).length > 0,
          env: typeof process.env.ANTHROPIC_AUTH_TOKEN === 'string' && process.env.ANTHROPIC_AUTH_TOKEN.length > 0,
          store: storeAnthropicAuthTokenPresent(),
        };
        const ok = sources.config || sources.env || sources.store;
        if (!ok) {
          const status = tier === 'default' ? 'fail' : 'warn';
          return { status, detail: `${tier}: anthropic/${modelName} (auth_mode=oauth, auth_token=${formatSources(sources)} — set ANTHROPIC_AUTH_TOKEN or run flynn anthropic-auth --token)` };
        }
        return { status: 'pass', detail: `${tier}: anthropic/${modelName} (auth_mode=oauth, auth_token=${formatSources(sources)})` };
      }

      const sources = {
        config: typeof cfg.api_key === 'string' && (cfg.api_key as string).length > 0,
        env: typeof process.env.ANTHROPIC_API_KEY === 'string' && process.env.ANTHROPIC_API_KEY.length > 0,
        store: storeAnthropicApiKeyPresent(),
      };

      const ok = sources.config || sources.env || sources.store;
      if (!ok) {
        const status = (authMode === 'api_key' && tier === 'default') ? 'fail' : 'warn';
        const hint = authMode === 'api_key'
          ? 'Set ANTHROPIC_API_KEY or run flynn anthropic-auth'
          : 'Set ANTHROPIC_API_KEY (or run flynn anthropic-auth) or set ANTHROPIC_AUTH_TOKEN (or run flynn anthropic-auth --token)';
        return { status, detail: `${tier}: anthropic/${modelName} (auth_mode=${authMode}, api_key=${formatSources(sources)} — ${hint})` };
      }
      return { status: 'pass', detail: `${tier}: anthropic/${modelName} (auth_mode=${authMode}, api_key=${formatSources(sources)})` };
    }

    // Providers with API-key style auth (no auth store integration yet)
    const needsKey = ['gemini', 'openrouter', 'vercel', 'xai', 'github'];
    if (needsKey.includes(provider)) {
      const envVarMap: Record<string, string> = {
        gemini: 'GEMINI_API_KEY',
        openrouter: 'OPENROUTER_API_KEY',
        vercel: 'AI_GATEWAY_API_KEY',
        xai: 'XAI_API_KEY',
        github: 'GITHUB_TOKEN',
      };
      const envVar = envVarMap[provider];
      const sources = {
        config: typeof cfg.api_key === 'string' && (cfg.api_key as string).length > 0,
        env: Boolean(envVar && process.env[envVar] && process.env[envVar]!.length > 0),
        store: false,
      };
      const ok = sources.config || sources.env;
      if (!ok) {
        const status = tier === 'default' ? 'warn' : 'warn';
        const hint = envVar ? `set ${envVar} or provide api_key in config` : 'provide api_key in config';
        return { status, detail: `${tier}: ${provider}/${modelName} (api_key=${formatSources(sources)} — ${hint})` };
      }
      return { status: 'pass', detail: `${tier}: ${provider}/${modelName} (api_key=${formatSources(sources)})` };
    }

    return { status: 'pass', detail: `${tier}: ${provider}/${modelName}` };
  };

  const tierEntries: Array<{ tier: string; cfg?: Record<string, unknown> }> = [
    { tier: 'default', cfg: models.default as unknown as Record<string, unknown> },
    { tier: 'fast', cfg: models.fast as unknown as Record<string, unknown> | undefined },
    { tier: 'complex', cfg: models.complex as unknown as Record<string, unknown> | undefined },
    { tier: 'local', cfg: models.local as unknown as Record<string, unknown> | undefined },
  ];

  const details: string[] = [];
  let hasFail = false;
  let hasWarn = false;
  for (const { tier, cfg } of tierEntries) {
    if (!cfg) {continue;}
    const r = checkTierAuth(tier, cfg);
    if (r.detail) {details.push(r.detail);}
    if (r.status === 'fail') {hasFail = true;}
    if (r.status === 'warn') {hasWarn = true;}
  }

  // Build a summary of the model stack
  details.push(`fallback: [${models.fallback_chain.join(', ')}]`);

  const status: CheckResult['status'] = hasFail ? 'fail' : hasWarn ? 'warn' : 'pass';
  return { status, label: 'Model connectivity', detail: details.join(', ') };
};

const checkTelegram: Check = async (ctx) => {
  if (!ctx.config) {
    return { status: 'skip', label: 'Telegram bot configured', detail: '(config invalid)' };
  }
  if (!ctx.config.telegram) {
    return { status: 'skip', label: 'Telegram bot configured', detail: '(not configured)' };
  }
  if (!ctx.config.telegram.bot_token || ctx.config.telegram.bot_token.length < 10) {
    return { status: 'warn', label: 'Telegram bot configured', detail: 'token looks too short' };
  }
  return { status: 'pass', label: 'Telegram bot configured', detail: `(${ctx.config.telegram.allowed_chat_ids.length} allowed chat(s))` };
};

const checkMcpServers: Check = async (ctx) => {
  if (!ctx.config) {
    return { status: 'skip', label: 'MCP servers configured', detail: '(config invalid)' };
  }
  const servers = ctx.config.mcp.servers;
  if (servers.length === 0) {
    return { status: 'skip', label: 'MCP servers configured', detail: '(none configured)' };
  }
  return { status: 'pass', label: 'MCP servers configured', detail: `(${servers.length} server(s))` };
};

const checkSkills: Check = async (ctx) => {
  if (!ctx.config) {
    return { status: 'skip', label: 'Skills loaded', detail: '(config invalid)' };
  }
  try {
    const skillDirs = {
      bundled: ctx.config.skills.bundled_dir,
      managed: ctx.config.skills.managed_dir,
      workspace: ctx.config.skills.workspace_dir,
    };
    const missingDirs = Object.entries(skillDirs)
      .filter(([, dir]) => Boolean(dir) && !existsSync(dir as string))
      .map(([tier, dir]) => `${tier}:${dir as string}`);

    const { loadAllSkills } = await import('../skills/index.js');
    const skills = loadAllSkills({
      bundledDir: skillDirs.bundled,
      managedDir: skillDirs.managed,
      workspaceDir: skillDirs.workspace,
    });

    const available = skills.filter((skill) => skill.available).length;
    const unavailable = skills.length - available;
    const detailParts = [`${skills.length} skill(s), ${available} available, ${unavailable} unavailable`];
    if (missingDirs.length > 0) {
      detailParts.push(`missing dirs: ${missingDirs.join(', ')}`);
      return { status: 'warn', label: 'Skills loaded', detail: detailParts.join(' — ') };
    }

    return { status: 'pass', label: 'Skills loaded', detail: detailParts.join(' — ') };
  } catch (err) {
    return { status: 'fail', label: 'Skills loaded', detail: err instanceof Error ? err.message : String(err) };
  }
};

const checkTailscale: Check = async (ctx) => {
  if (!ctx.config?.server?.tailscale?.serve) {
    return { status: 'skip', label: 'Tailscale Serve', detail: '(not enabled)' };
  }
  try {
    const { isTailscaleAvailable } = await import('../gateway/tailscale.js');
    const result = await isTailscaleAvailable();
    if (result.available) {
      return { status: 'pass', label: 'Tailscale Serve', detail: `(v${result.version})` };
    }
    return { status: 'fail', label: 'Tailscale Serve', detail: result.error ?? 'Tailscale not available' };
  } catch (err) {
    return { status: 'fail', label: 'Tailscale Serve', detail: err instanceof Error ? err.message : String(err) };
  }
};

function expandPath(p: string): string {
  if (p.startsWith('~/') || p === '~') {
    return resolve(homedir(), p.slice(2));
  }
  return resolve(p);
}

const checkGmail: Check = async (ctx) => {
  if (!ctx.config) {
    return { status: 'skip', label: 'Gmail configured', detail: '(config invalid)' };
  }
  const gmail = ctx.config.automation.gmail;
  if (!gmail?.enabled) {
    return { status: 'skip', label: 'Gmail configured', detail: '(not enabled)' };
  }

  const credentialsPath = expandPath(gmail.credentials_file ?? '~/.config/flynn/gmail-credentials.json');
  if (!existsSync(credentialsPath)) {
    return { status: 'fail', label: 'Gmail configured', detail: `credentials file not found: ${credentialsPath}` };
  }

  let googleProjectId: string | undefined;
  try {
    const creds = JSON.parse(readFileSync(credentialsPath, 'utf-8')) as Record<string, unknown>;
    const installed = (creds.installed as Record<string, unknown> | undefined) ?? (creds.web as Record<string, unknown> | undefined);
    const projectId = installed?.project_id;
    if (typeof projectId === 'string' && projectId.trim()) {
      googleProjectId = projectId.trim();
    }
  } catch {
    // Ignore JSON parse errors; doctor will still validate token and output.
  }

  const tokenPath = expandPath(gmail.token_file ?? '~/.config/flynn/gmail-token.json');
  if (!existsSync(tokenPath)) {
    return { status: 'warn', label: 'Gmail configured', detail: 'run `flynn gmail-auth` to authenticate' };
  }

  const modes: string[] = [];
  const warnings: string[] = [];

  const topicRaw = (gmail.pubsub_topic ?? process.env.FLYNN_GMAIL_PUBSUB_TOPIC ?? '').trim();
  const pushEnabled = Boolean(topicRaw) && !gmail.disable_push;
  if (pushEnabled) {
    modes.push('push');
    if (topicRaw.includes('/')) {
      const ok = /^projects\/[^/]+\/topics\/[^/]+$/.test(topicRaw);
      if (!ok) {
        warnings.push('pubsub_topic format invalid (expected projects/<project>/topics/<topic>)');
      }
    } else if (!googleProjectId) {
      warnings.push('pubsub_topic shorthand requires project_id in Gmail credentials');
    }

    if (ctx.config.server?.tailscale?.serve) {
      warnings.push('push requires a public HTTPS endpoint; Tailscale Serve is typically tailnet-only');
    }
  } else if (gmail.disable_push && topicRaw) {
    warnings.push('push disabled (disable_push=true)');
  }

  const subRaw = (gmail.pubsub_subscription_id ?? '').trim();
  if (subRaw) {
    modes.push('pull');
    if (subRaw.includes('/')) {
      const ok = /^projects\/[^/]+\/subscriptions\/[^/]+$/.test(subRaw);
      if (!ok) {
        warnings.push('pubsub_subscription_id format invalid (expected projects/<project>/subscriptions/<sub>)');
      }
    } else if (!googleProjectId) {
      warnings.push('pubsub_subscription_id shorthand requires project_id in Gmail credentials');
    }
  }

  modes.push('poll');
  const detail = `(${modes.join(' + ')} -> ${gmail.output.channel}/${gmail.output.peer})`;
  const withWarnings = warnings.length > 0 ? `${detail} — ${warnings.join('; ')}` : detail;

  return { status: warnings.length > 0 ? 'warn' : 'pass', label: 'Gmail configured', detail: withWarnings };
};

const allChecks: Check[] = [
  checkConfigExists,
  checkOverlayExists,
  checkConfigParses,
  checkDeprecatedConfigKeys,
  checkConfigValidates,
  checkEnvVars,
  checkDataDir,
  checkSessionDb,
  checkModelConnectivity,
  checkTelegram,
  checkGmail,
  checkMcpServers,
  checkSkills,
  checkTailscale,
];

/** Run all doctor checks in order. Exported for testing. */
export async function runChecks(ctx: DoctorContext): Promise<CheckResult[]> {
  const results: CheckResult[] = [];
  for (const check of allChecks) {
    const result = await check(ctx);
    results.push(result);
  }
  return results;
}

export function registerDoctorCommand(program: Command): void {
  program
    .command('doctor')
    .description('Validate configuration and check system health')
    .option('-c, --config <path>', 'Config file path')
    .action(async (opts: { config?: string }) => {
      const configPath = opts.config ?? getConfigPath();
      const dataDir = getDataDir();

      console.log('Flynn Doctor');
      console.log('============');
      console.log('');

      const ctx: DoctorContext = { configPath, dataDir };
      const results = await runChecks(ctx);

      for (const result of results) {
        console.log(formatStatus(result.status, result.label, result.detail));
      }

      console.log('');

      const counts = {
        pass: results.filter(r => r.status === 'pass').length,
        fail: results.filter(r => r.status === 'fail').length,
        warn: results.filter(r => r.status === 'warn').length,
        skip: results.filter(r => r.status === 'skip').length,
      };

      console.log(`Results: ${counts.pass} passed, ${counts.fail} failed, ${counts.warn} warnings, ${counts.skip} skipped`);

      process.exit(counts.fail > 0 ? 1 : 0);
    });
}
