import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as yaml from 'js-yaml';
import { MANAGED_PLUGINS, REQUIRED_MARKDOWN_EXTENSIONS } from './constants';

const MERMAID_FENCE_TAG = '!!python/name:pymdownx.superfences.fence_code_format';

const DEFAULT_PALETTE = [
  {
    media: '(prefers-color-scheme: light)',
    scheme: 'default',
    toggle: { icon: 'material/brightness-7', name: 'Switch to dark mode' },
  },
  {
    media: '(prefers-color-scheme: dark)',
    scheme: 'slate',
    toggle: { icon: 'material/brightness-4', name: 'Switch to light mode' },
  },
];

const MERMAID_SUPERFENCES_CONFIG = {
  'pymdownx.superfences': {
    custom_fences: [
      {
        name: 'mermaid',
        class: 'mermaid',
        format: MERMAID_FENCE_TAG,
      },
    ],
  },
};

/**
 * Preprocess YAML text to escape tags that js-yaml cannot handle (e.g.
 * !!python/name:…).  techdocs-preview.sh uses yq which handles these
 * natively; js-yaml does not, so we replace them with a sentinel that
 * survives a parse→dump round-trip, then restore them in postprocessing.
 */
const YAML_TAG_RE = /!!python\/name:(\S+)/g;
const YAML_TAG_SENTINEL = '__PYTAG__';
const YAML_SENTINEL_RE = new RegExp(`${YAML_TAG_SENTINEL}(\\S+)`, 'g');

function preprocessYaml(raw: string): string {
  return raw.replace(YAML_TAG_RE, `${YAML_TAG_SENTINEL}$1`);
}

function postprocessYaml(dumped: string): string {
  return dumped.replace(YAML_SENTINEL_RE, '!!python/name:$1');
}

interface MkDocsConfig {
  plugins?: Array<string | Record<string, unknown>>;
  theme?: string | Record<string, unknown>;
  markdown_extensions?: Array<string | Record<string, unknown>>;
  hooks?: string[];
  [key: string]: unknown;
}

function getPluginName(plugin: string | Record<string, unknown>): string {
  if (typeof plugin === 'string') { return plugin; }
  return Object.keys(plugin)[0] ?? '';
}

function extractUserPlugins(config: MkDocsConfig): Array<string | Record<string, unknown>> {
  if (!config.plugins) { return []; }
  return config.plugins.filter((p) => {
    const name = getPluginName(p);
    return !MANAGED_PLUGINS.some((m) => name === m || name.endsWith(`-${m}`));
  });
}

function buildPluginsBlock(
  themeName: string,
  docsDir: string,
  websocketPort: number,
  apiPort: number,
  autoGenerated: boolean
): Array<string | Record<string, unknown>> {
  const plugins: Array<string | Record<string, unknown>> = ['search'];

  if (themeName === 'material') {
    plugins.push({
      'techdocs-core': {
        use_material_search: true,
        use_pymdownx_blocks: true,
      },
    });
  }

  if (autoGenerated) {
    plugins.push('mkdocs-nav-weight');
  }

  plugins.push({
    'live-edit': {
      user_docs_dir: docsDir,
      websockets_port: websocketPort,
    },
  });

  const wysiwygConfig: Record<string, unknown> = {};
  if (apiPort > 0) {
    wysiwygConfig.api_port = apiPort;
  }
  plugins.push(
    Object.keys(wysiwygConfig).length > 0
      ? { 'live-wysiwyg': wysiwygConfig }
      : 'live-wysiwyg'
  );

  return plugins;
}

function resolveThemeName(config: MkDocsConfig, useUserTheme: boolean): string {
  if (!useUserTheme) { return 'material'; }
  const theme = config.theme;
  if (typeof theme === 'string') { return theme; }
  if (theme && typeof theme === 'object' && 'name' in theme) {
    return String(theme.name);
  }
  return 'material';
}

function applyTheme(rendered: MkDocsConfig, useUserTheme: boolean, themeName: string): void {
  if (useUserTheme) {
    const userTheme = rendered.theme;
    if (typeof userTheme === 'string') {
      rendered.theme = { name: userTheme };
    }
    const materialBase: Record<string, unknown> = { name: 'material' };
    if (rendered.theme && typeof rendered.theme === 'object') {
      rendered.theme = { ...materialBase, ...rendered.theme };
    } else {
      rendered.theme = materialBase;
    }
    if (themeName === 'material') {
      const themeObj = rendered.theme as Record<string, unknown>;
      if (!themeObj.palette) {
        themeObj.palette = DEFAULT_PALETTE;
      }
    }
  } else {
    rendered.theme = {
      name: 'material',
      palette: DEFAULT_PALETTE,
    };
  }
}

function addMarkdownExtensionIfMissing(config: MkDocsConfig, ext: string): void {
  if (!config.markdown_extensions) {
    config.markdown_extensions = [];
  }
  const existing = config.markdown_extensions.some((e) => {
    const name = typeof e === 'string' ? e : Object.keys(e)[0];
    return name === ext;
  });
  if (!existing) {
    config.markdown_extensions.push(ext);
  }
}

function addSuperfencesWithMermaidIfMissing(config: MkDocsConfig): void {
  if (!config.markdown_extensions) {
    config.markdown_extensions = [];
  }
  const hasSuperfences = config.markdown_extensions.some((e) => {
    const name = typeof e === 'string' ? e : Object.keys(e)[0];
    return name === 'pymdownx.superfences';
  });
  if (!hasSuperfences) {
    config.markdown_extensions.push(MERMAID_SUPERFENCES_CONFIG);
  }
}

function generateRestoreThemeHook(tmpDir: string, themeConfig: Record<string, unknown>): string {
  const themeConfigPath = path.join(tmpDir, 'theme-config.yml');
  fs.writeFileSync(themeConfigPath, yaml.dump(themeConfig));
  const hookPath = path.join(tmpDir, 'restore_theme.py');
  fs.writeFileSync(hookPath, `import os
import yaml

def on_config(config):
    theme_file = os.path.join(os.environ.get('TMP_DIR', ''), 'theme-config.yml')
    if not os.path.exists(theme_file):
        return config
    with open(theme_file) as f:
        saved_theme = yaml.safe_load(f)
    if not saved_theme:
        return config
    for key, value in saved_theme.items():
        if key != 'name':
            config['theme'][key] = value
    return config
`);
  return hookPath;
}

export interface GenerateConfigOptions {
  workspaceDir: string;
  tmpDir: string;
  websocketPort: number;
  apiPort: number;
  useUserTheme: boolean;
  autoGenerated: boolean;
}

export function generateMkdocsConfig(options: GenerateConfigOptions): string {
  const { workspaceDir, tmpDir, websocketPort, apiPort, useUserTheme, autoGenerated } = options;
  const mkdocsYmlPath = path.join(workspaceDir, 'mkdocs.yml');
  const originalContent = fs.readFileSync(mkdocsYmlPath, 'utf8');

  fs.mkdirSync(tmpDir, { recursive: true });
  fs.writeFileSync(path.join(tmpDir, 'original-mkdocs.yml'), originalContent);

  const sanitized = preprocessYaml(originalContent);
  let config: MkDocsConfig;
  try {
    config = yaml.load(sanitized) as MkDocsConfig ?? {};
  } catch {
    config = {};
  }

  const themeName = resolveThemeName(config, useUserTheme);
  const userPlugins = extractUserPlugins(config);

  const docsDir = path.join(workspaceDir, 'docs');
  const pluginsBlock = buildPluginsBlock(themeName, docsDir, websocketPort, apiPort, autoGenerated);

  const rendered: MkDocsConfig = { ...config };
  delete rendered.plugins;

  rendered.plugins = [...pluginsBlock, ...userPlugins];

  applyTheme(rendered, useUserTheme, themeName);

  if (themeName === 'material') {
    const themeObj = rendered.theme as Record<string, unknown>;
    const hookPath = generateRestoreThemeHook(tmpDir, themeObj);
    rendered.hooks = [...(rendered.hooks ?? []), hookPath];
  }

  for (const ext of REQUIRED_MARKDOWN_EXTENSIONS) {
    addMarkdownExtensionIfMissing(rendered, ext);
  }
  addSuperfencesWithMermaidIfMissing(rendered);

  const banner = [
    '################################################################################',
    '# DO NOT EDIT THIS FILE while the server is running.',
    '#',
    '# WARNING: This entire mkdocs.yml file will be deleted. The original will be',
    '# restored overwriting this file when the server shuts down.',
    '#',
    '# If you want to edit your mkdocs.yml, do so after shutting down the server.',
    '################################################################################',
    '',
  ].join(os.EOL);

  const rawYaml = yaml.dump(rendered, {
    lineWidth: -1,
    noRefs: true,
    quotingType: '"',
    forceQuotes: false,
  });

  const yamlContent = postprocessYaml(rawYaml);
  const generatedContent = banner + yamlContent;

  fs.writeFileSync(mkdocsYmlPath, generatedContent);

  return generatedContent;
}

export function restoreOriginalConfig(workspaceDir: string, tmpDir: string): void {
  const originalPath = path.join(tmpDir, 'original-mkdocs.yml');
  const mkdocsYmlPath = path.join(workspaceDir, 'mkdocs.yml');
  if (fs.existsSync(originalPath)) {
    fs.copyFileSync(originalPath, mkdocsYmlPath);
  }
}
