import fs from 'node:fs';
import path from 'node:path';
import { buildConfigSettingsSnapshot } from '../../config/settings/jsonc-edit';
import type { ConfigValidationWarning, RawConfig, ResolvedConfig } from '../../types/config';
import type {
  ConfigSettingsAnkiDeckResult,
  ConfigSettingsAnkiListResult,
  ConfigSettingsField,
  ConfigSettingsSaveResult,
  ConfigSettingsSnapshot,
} from '../../types/settings';
import type { ReloadConfigStrictResult } from '../../config';
import { classifyConfigHotReloadDiff } from '../../core/services/config-hot-reload';
import {
  createSaveConfigSettingsPatchHandler,
  type ConfigSettingsHotReloadDiff,
} from './config-settings-save';
import {
  createOpenConfigSettingsWindowHandler,
  type ConfigSettingsWindowLike,
} from './config-settings-window';
import { isConfigSettingsPatch } from './config-settings-ipc';

export interface ConfigSettingsIpcMainLike {
  handle(channel: string, listener: (event: unknown, ...args: unknown[]) => unknown): unknown;
}

export interface ConfigSettingsIpcChannels {
  getConfigSettingsSnapshot: string;
  saveConfigSettingsPatch: string;
  openConfigSettingsFile: string;
  openConfigSettingsWindow: string;
  getConfigSettingsAnkiDeckNames: string;
  getConfigSettingsAnkiDeckFieldNames: string;
  getConfigSettingsAnkiDeckModelNames: string;
  getConfigSettingsAnkiModelNames: string;
  getConfigSettingsAnkiModelFieldNames: string;
  getConfigSettingsYomitanAnkiDeckName: string;
}

export interface ConfigSettingsAnkiClient {
  deckNames(): Promise<string[]>;
  fieldNamesForDeck(deckName: string): Promise<string[]>;
  modelNamesForDeck(deckName: string): Promise<string[]>;
  modelNames(): Promise<string[]>;
  modelFieldNames(modelName: string): Promise<string[]>;
}

export interface ConfigSettingsRuntimeDeps<TWindow extends ConfigSettingsWindowLike> {
  fields: ConfigSettingsField[];
  getConfigPath(): string;
  getRawConfig(): RawConfig;
  getConfig(): ResolvedConfig;
  getWarnings(): ConfigValidationWarning[];
  reloadConfigStrict(): ReloadConfigStrictResult;
  onHotReloadApplied?: (diff: ConfigSettingsHotReloadDiff, config: ResolvedConfig) => void;
  getSettingsWindow(): TWindow | null;
  setSettingsWindow(window: TWindow | null): void;
  createSettingsWindow(): TWindow;
  settingsHtmlPath: string;
  promoteSettingsWindowAboveOverlay?: (window: TWindow) => void;
  openPath(path: string): Promise<string>;
  defaultAnkiConnectUrl: string;
  createAnkiClient(url: string): ConfigSettingsAnkiClient;
  getYomitanAnkiDeckName?: () => Promise<string | null | undefined>;
  ipcMain: ConfigSettingsIpcMainLike;
  ipcChannels: ConfigSettingsIpcChannels;
  log?: (message: string) => void;
}

export function writeTextFileAtomically(targetPath: string, content: string): void {
  fs.mkdirSync(path.dirname(targetPath), { recursive: true });
  const tempPath = path.join(
    path.dirname(targetPath),
    `.${path.basename(targetPath)}.${process.pid}.${Date.now()}.tmp`,
  );
  try {
    fs.writeFileSync(tempPath, content, 'utf-8');
    fs.renameSync(tempPath, targetPath);
  } catch (error) {
    try {
      fs.rmSync(tempPath, { force: true });
    } catch {
      // Best effort cleanup after a failed atomic write.
    }
    throw error;
  }
}

function getRestartRequiredSettingsSections(
  fields: readonly ConfigSettingsField[],
  restartRequiredFields: string[],
): string[] {
  const sections = new Set<string>();
  for (const field of fields) {
    if (
      restartRequiredFields.some(
        (restartField) =>
          field.configPath === restartField ||
          field.configPath.startsWith(`${restartField}.`) ||
          restartField.startsWith(`${field.configPath}.`),
      )
    ) {
      sections.add(field.section);
    }
  }
  return [...sections].sort();
}

export function createConfigSettingsRuntime<TWindow extends ConfigSettingsWindowLike>(
  deps: ConfigSettingsRuntimeDeps<TWindow>,
) {
  function getSnapshot(): ConfigSettingsSnapshot {
    return buildConfigSettingsSnapshot({
      configPath: deps.getConfigPath(),
      rawConfig: deps.getRawConfig(),
      resolvedConfig: deps.getConfig(),
      warnings: deps.getWarnings(),
      fields: deps.fields,
    });
  }

  const savePatch = createSaveConfigSettingsPatchHandler({
    getConfigPath: () => deps.getConfigPath(),
    getCurrentConfig: () => deps.getConfig(),
    getWarnings: () => deps.getWarnings(),
    getSnapshot,
    fileExists: (targetPath) => fs.existsSync(targetPath),
    readText: (targetPath) => fs.readFileSync(targetPath, 'utf-8'),
    writeTextAtomically: (targetPath, content) => writeTextFileAtomically(targetPath, content),
    deleteFile: (targetPath) => fs.rmSync(targetPath, { force: true }),
    reloadConfigStrict: () => deps.reloadConfigStrict(),
    classifyDiff: (previous, next) => classifyConfigHotReloadDiff(previous, next),
    getRestartRequiredSections: (fields) => getRestartRequiredSettingsSections(deps.fields, fields),
    onHotReloadApplied: deps.onHotReloadApplied,
  });

  function ensureConfigFileExists(): string {
    const configPath = deps.getConfigPath();
    if (!fs.existsSync(configPath)) {
      writeTextFileAtomically(configPath, '{}\n');
    }
    return configPath;
  }

  const openWindow = createOpenConfigSettingsWindowHandler({
    getSettingsWindow: deps.getSettingsWindow,
    setSettingsWindow: deps.setSettingsWindow,
    createSettingsWindow: deps.createSettingsWindow,
    settingsHtmlPath: deps.settingsHtmlPath,
    promoteSettingsWindowAboveOverlay: deps.promoteSettingsWindowAboveOverlay,
    log: deps.log,
  });

  function invalidPatchResult(): ConfigSettingsSaveResult {
    return {
      ok: false,
      warnings: [],
      error: 'Invalid config settings patch.',
      hotReloadFields: [],
      restartRequiredFields: [],
      restartRequiredSections: [],
    };
  }

  function getAnkiConnectUrl(draftUrl: unknown): string {
    return typeof draftUrl === 'string' && draftUrl.trim().length > 0
      ? draftUrl.trim()
      : deps.getConfig().ankiConnect.url || deps.defaultAnkiConnectUrl;
  }

  async function getAnkiList(
    draftUrl: unknown,
    lookup: (client: ConfigSettingsAnkiClient) => Promise<string[]>,
  ): Promise<ConfigSettingsAnkiListResult> {
    try {
      const client = deps.createAnkiClient(getAnkiConnectUrl(draftUrl));
      return { ok: true, values: await lookup(client) };
    } catch (error) {
      return {
        ok: false,
        values: [],
        error: error instanceof Error ? error.message : 'Failed to query AnkiConnect.',
      };
    }
  }

  function invalidAnkiListResult(error: string): ConfigSettingsAnkiListResult {
    return {
      ok: false,
      values: [],
      error,
    };
  }

  function persistInferredYomitanDeckIfEmpty(deckName: string): void {
    const normalizedDeckName = deckName.trim();
    const configuredDeckName = deps.getConfig().ankiConnect?.deck?.trim() ?? '';
    if (!normalizedDeckName || configuredDeckName) {
      return;
    }

    const result = savePatch({
      operations: [
        {
          op: 'set',
          path: 'ankiConnect.deck',
          value: normalizedDeckName,
        },
      ],
    });
    if (!result.ok) {
      deps.log?.(
        `Failed to persist inferred Yomitan Anki deck: ${result.error ?? 'unknown error'}`,
      );
    }
  }

  async function getYomitanAnkiDeckName(): Promise<ConfigSettingsAnkiDeckResult> {
    if (!deps.getYomitanAnkiDeckName) {
      return { ok: true, value: '' };
    }
    try {
      const value = await deps.getYomitanAnkiDeckName();
      const deckName = typeof value === 'string' ? value.trim() : '';
      persistInferredYomitanDeckIfEmpty(deckName);
      return { ok: true, value: deckName };
    } catch (error) {
      return {
        ok: false,
        value: '',
        error: error instanceof Error ? error.message : 'Failed to query Yomitan.',
      };
    }
  }

  function registerHandlers(): void {
    deps.ipcMain.handle(deps.ipcChannels.getConfigSettingsSnapshot, () => getSnapshot());
    deps.ipcMain.handle(deps.ipcChannels.saveConfigSettingsPatch, (_event, patch: unknown) => {
      if (!isConfigSettingsPatch(patch, deps.fields)) {
        return invalidPatchResult();
      }
      return savePatch(patch);
    });
    deps.ipcMain.handle(deps.ipcChannels.openConfigSettingsFile, async () => {
      const openError = await deps.openPath(ensureConfigFileExists());
      return openError.length === 0;
    });
    deps.ipcMain.handle(deps.ipcChannels.openConfigSettingsWindow, () => openWindow());
    deps.ipcMain.handle(deps.ipcChannels.getConfigSettingsAnkiDeckNames, (_event, draftUrl) =>
      getAnkiList(draftUrl, (client) => client.deckNames()),
    );
    deps.ipcMain.handle(
      deps.ipcChannels.getConfigSettingsAnkiDeckFieldNames,
      (_event, deckName, draftUrl) => {
        const normalizedDeckName = typeof deckName === 'string' ? deckName.trim() : '';
        return normalizedDeckName
          ? getAnkiList(draftUrl, (client) => client.fieldNamesForDeck(normalizedDeckName))
          : invalidAnkiListResult('Deck name is required.');
      },
    );
    deps.ipcMain.handle(
      deps.ipcChannels.getConfigSettingsAnkiDeckModelNames,
      (_event, deckName, draftUrl) => {
        const normalizedDeckName = typeof deckName === 'string' ? deckName.trim() : '';
        return normalizedDeckName
          ? getAnkiList(draftUrl, (client) => client.modelNamesForDeck(normalizedDeckName))
          : invalidAnkiListResult('Deck name is required.');
      },
    );
    deps.ipcMain.handle(deps.ipcChannels.getConfigSettingsAnkiModelNames, (_event, draftUrl) =>
      getAnkiList(draftUrl, (client) => client.modelNames()),
    );
    deps.ipcMain.handle(
      deps.ipcChannels.getConfigSettingsAnkiModelFieldNames,
      (_event, modelName, draftUrl) => {
        const normalizedModelName = typeof modelName === 'string' ? modelName.trim() : '';
        return normalizedModelName
          ? getAnkiList(draftUrl, (client) => client.modelFieldNames(normalizedModelName))
          : invalidAnkiListResult('Note type is required.');
      },
    );
    deps.ipcMain.handle(deps.ipcChannels.getConfigSettingsYomitanAnkiDeckName, () =>
      getYomitanAnkiDeckName(),
    );
  }

  return {
    getSnapshot,
    savePatch,
    openWindow,
    registerHandlers,
  };
}
