import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import type { Config, ModelConfig } from '../../config/index.js';

const execFile = promisify(execFileCb);

export type LocalBackendId = 'ollama' | 'llamacpp';
export type LocalBackendAction = 'start' | 'restart' | 'stop' | 'update';

export interface LocalBackendStatus {
  id: LocalBackendId;
  provider: 'ollama' | 'llamacpp';
  name: string;
  unit: string;
  configured: boolean;
  loadState: string;
  activeState: string;
  subState: string;
  unitFileState: string;
  description: string;
  pid: number | null;
  result: string;
  statusText: string;
  availableActions: LocalBackendAction[];
  error?: string;
}

export interface LocalBackendControlResult {
  backend: LocalBackendId;
  action: LocalBackendAction;
  status: LocalBackendStatus;
  message?: string;
  updatedModels?: string[];
}

type SystemctlResult = { stdout: string; stderr: string };
type SystemctlRunner = (args: string[]) => Promise<SystemctlResult>;

const LOCAL_BACKEND_UNITS: Record<LocalBackendId, {
  provider: 'ollama' | 'llamacpp';
  name: string;
  unit: string;
}> = {
  ollama: {
    provider: 'ollama',
    name: 'Ollama',
    unit: 'ollama.service',
  },
  llamacpp: {
    provider: 'llamacpp',
    name: 'llama.cpp server',
    unit: 'llama-server.service',
  },
};

function defaultRunner(args: string[]): Promise<SystemctlResult> {
  return execFile('systemctl', args, { timeout: 10_000, maxBuffer: 1024 * 1024 }) as Promise<SystemctlResult>;
}

function collectModelConfigs(config: Config): ModelConfig[] {
  const models: ModelConfig[] = [config.models.default];
  if (config.models.local) {models.push(config.models.local);}
  if (config.models.fast) {models.push(config.models.fast);}
  if (config.models.complex) {models.push(config.models.complex);}
  if (config.models.local_providers) {
    models.push(...Object.values(config.models.local_providers));
  }
  return models;
}

function collectConfiguredLocalBackends(config: Config): Set<LocalBackendId> {
  const configured = new Set<LocalBackendId>();

  for (const model of collectModelConfigs(config)) {
    if (model.provider === 'ollama') {
      configured.add('ollama');
    }
    if (model.provider === 'llamacpp') {
      configured.add('llamacpp');
    }
  }

  const embeddingProvider = config.memory.embedding.provider;
  if (embeddingProvider === 'ollama') {
    configured.add('ollama');
  }
  if (embeddingProvider === 'llamacpp') {
    configured.add('llamacpp');
  }

  if (config.audio.enabled) {
    const audioProvider = config.audio.provider?.type;
    if (audioProvider === 'ollama') {
      configured.add('ollama');
    }
    if (audioProvider === 'llamacpp') {
      configured.add('llamacpp');
    }
  }

  return configured;
}

function parseKeyValueOutput(output: string): Record<string, string> {
  const result: Record<string, string> = {};
  for (const line of output.split('\n')) {
    if (!line.trim()) {continue;}
    const idx = line.indexOf('=');
    if (idx <= 0) {continue;}
    const key = line.slice(0, idx).trim();
    const value = line.slice(idx + 1).trim();
    result[key] = value;
  }
  return result;
}

function normalizeError(error: unknown): string {
  if (error && typeof error === 'object') {
    const maybe = error as { stderr?: string; stdout?: string; message?: string };
    const stderr = maybe.stderr?.trim();
    if (stderr) {return stderr;}
    const stdout = maybe.stdout?.trim();
    if (stdout) {return stdout;}
    if (typeof maybe.message === 'string' && maybe.message.trim()) {return maybe.message.trim();}
  }
  if (error instanceof Error && error.message.trim()) {
    return error.message.trim();
  }
  return String(error);
}

function isUnitMissing(errorText: string): boolean {
  return (
    errorText.includes('could not be found')
    || errorText.includes('not-found')
    || errorText.includes('No such file or directory')
  );
}

function computeAvailableActions(activeState: string, loadState: string, configured: boolean): LocalBackendAction[] {
  if (loadState === 'not-found') {
    return [];
  }
  const withUpdate = (actions: LocalBackendAction[]): LocalBackendAction[] => {
    if (configured && !actions.includes('update')) {
      return [...actions, 'update'];
    }
    return actions;
  };
  if (activeState === 'active') {
    return withUpdate(['restart', 'stop']);
  }
  if (activeState === 'inactive' || activeState === 'failed') {
    return withUpdate(['start', 'restart']);
  }
  if (activeState === 'activating' || activeState === 'deactivating') {
    return withUpdate(['restart', 'stop']);
  }
  return withUpdate(['start', 'restart', 'stop']);
}

function buildStatusText(activeState: string, subState: string): string {
  if (activeState === 'active' && subState) {
    return `${activeState} (${subState})`;
  }
  if (activeState && subState && activeState !== subState) {
    return `${activeState}/${subState}`;
  }
  return activeState || subState || 'unknown';
}

async function fetchUnitStatus(
  id: LocalBackendId,
  configured: boolean,
  runner: SystemctlRunner,
): Promise<LocalBackendStatus> {
  const unitDef = LOCAL_BACKEND_UNITS[id];
  const base: Omit<LocalBackendStatus, 'availableActions' | 'statusText'> = {
    id,
    provider: unitDef.provider,
    name: unitDef.name,
    unit: unitDef.unit,
    configured,
    loadState: 'unknown',
    activeState: 'unknown',
    subState: 'unknown',
    unitFileState: 'unknown',
    description: unitDef.name,
    pid: null,
    result: 'unknown',
  };

  try {
    const response = await runner([
      '--user',
      'show',
      unitDef.unit,
      '--property=LoadState,ActiveState,SubState,UnitFileState,Description,ExecMainPID,Result',
      '--no-pager',
    ]);
    const parsed = parseKeyValueOutput(response.stdout);
    const pidRaw = parsed.ExecMainPID;
    const pid = pidRaw ? Number(pidRaw) : NaN;

    const status: LocalBackendStatus = {
      ...base,
      loadState: parsed.LoadState || base.loadState,
      activeState: parsed.ActiveState || base.activeState,
      subState: parsed.SubState || base.subState,
      unitFileState: parsed.UnitFileState || base.unitFileState,
      description: parsed.Description || base.description,
      pid: Number.isFinite(pid) && pid > 0 ? pid : null,
      result: parsed.Result || base.result,
      statusText: '',
      availableActions: [],
    };
    status.availableActions = computeAvailableActions(status.activeState, status.loadState, configured);
    status.statusText = buildStatusText(status.activeState, status.subState);
    return status;
  } catch (error) {
    const detail = normalizeError(error);
    if (isUnitMissing(detail)) {
      const missingStatus: LocalBackendStatus = {
        ...base,
        loadState: 'not-found',
        activeState: 'inactive',
        subState: 'dead',
        result: 'not-found',
        unitFileState: 'not-found',
        statusText: 'missing',
        availableActions: [],
        error: detail,
      };
      return missingStatus;
    }
    return {
      ...base,
      statusText: 'unknown',
      availableActions: computeAvailableActions(base.activeState, base.loadState, configured),
      error: detail,
    };
  }
}

function ensureValidAction(action: string): asserts action is LocalBackendAction {
  if (action !== 'start' && action !== 'restart' && action !== 'stop' && action !== 'update') {
    throw new Error(`Unsupported action: ${action}`);
  }
}

function ensureValidBackend(id: string): asserts id is LocalBackendId {
  if (id !== 'ollama' && id !== 'llamacpp') {
    throw new Error(`Unsupported backend: ${id}`);
  }
}

function collectBackendModels(config: Config, backend: LocalBackendId): string[] {
  const models = new Set<string>();

  for (const model of collectModelConfigs(config)) {
    if (model.provider === backend && typeof model.model === 'string' && model.model.trim().length > 0) {
      models.add(model.model.trim());
    }
  }

  if (config.memory.embedding.provider === backend) {
    const embeddingModel = config.memory.embedding.model?.trim();
    if (embeddingModel) {
      models.add(embeddingModel);
    }
  }

  const audioProvider = config.audio.provider;
  if (config.audio.enabled && audioProvider?.type === backend) {
    const audioModel = audioProvider.model?.trim();
    if (audioModel) {
      models.add(audioModel);
    }
  }

  return Array.from(models);
}

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function updateOllama(config: Config, runner: SystemctlRunner): Promise<{
  message: string;
  updatedModels?: string[];
}> {
  const models = collectBackendModels(config, 'ollama');
  if (models.length === 0) {
    return { message: 'No configured Ollama models were found to update.' };
  }

  const status = await fetchUnitStatus('ollama', true, runner);
  if (status.loadState !== 'not-found' && status.activeState !== 'active') {
    await runner(['--user', 'start', LOCAL_BACKEND_UNITS.ollama.unit, '--no-pager']);
    // Briefly wait for the daemon to accept pull requests.
    await delay(250);
  }

  const updatedModels: string[] = [];
  const failures: string[] = [];

  for (const model of models) {
    try {
      await execFile('ollama', ['pull', model], { timeout: 15 * 60_000, maxBuffer: 4 * 1024 * 1024 });
      updatedModels.push(model);
    } catch (error) {
      failures.push(`${model} (${normalizeError(error)})`);
    }
  }

  if (updatedModels.length === 0 && failures.length > 0) {
    throw new Error(`Failed to update configured Ollama models: ${failures.join('; ')}`);
  }

  if (failures.length > 0) {
    return {
      message: `Updated ${updatedModels.length} model(s); ${failures.length} failed.`,
      updatedModels,
    };
  }

  return {
    message: `Updated ${updatedModels.length} model(s).`,
    updatedModels,
  };
}

async function updateLlamaCpp(runner: SystemctlRunner): Promise<{ message: string }> {
  await runner(['--user', 'restart', LOCAL_BACKEND_UNITS.llamacpp.unit, '--no-pager']);
  return {
    message: 'Restarted llama.cpp service (model file updates are managed outside Flynn).',
  };
}

export async function listLocalBackendStatuses(
  config: Config,
  runner: SystemctlRunner = defaultRunner,
): Promise<LocalBackendStatus[]> {
  const configured = collectConfiguredLocalBackends(config);
  return Promise.all((Object.keys(LOCAL_BACKEND_UNITS) as LocalBackendId[]).map((id) =>
    fetchUnitStatus(id, configured.has(id), runner),
  ));
}

export async function controlLocalBackend(
  config: Config,
  backend: string,
  action: string,
  runner: SystemctlRunner = defaultRunner,
): Promise<LocalBackendControlResult> {
  ensureValidBackend(backend);
  ensureValidAction(action);

  let message: string | undefined;
  let updatedModels: string[] | undefined;

  if (action === 'update') {
    if (backend === 'ollama') {
      const result = await updateOllama(config, runner);
      message = result.message;
      updatedModels = result.updatedModels;
    } else {
      const result = await updateLlamaCpp(runner);
      message = result.message;
    }
  } else {
    const unitDef = LOCAL_BACKEND_UNITS[backend];
    await runner(['--user', action, unitDef.unit, '--no-pager']);
  }

  const configured = collectConfiguredLocalBackends(config);
  const status = await fetchUnitStatus(backend, configured.has(backend), runner);
  return {
    backend,
    action,
    status,
    message,
    updatedModels,
  };
}
