import path from 'node:path';
import fs from 'fs';
import { Config, ConfigModelProvider, UIConfigSections } from './types';
import { hashObj } from '../serverUtils';
import { loadModelProvidersUIConfigSection } from './providersLoader';

class ConfigManager {
  configPath: string = path.join(
    process.env.DATA_DIR
      ? path.resolve(process.cwd(), process.env.DATA_DIR)
      : process.cwd(),
    'data',
    'config.json',
  );
  configVersion = 1;
  currentConfig: Config = {
    version: this.configVersion,
    setupComplete: false,
    preferences: {},
    personalization: {},
    modelProviders: [],
    search: {
      searxngURL: '',
      newsRegion: 'auto',
    },
  };
  uiConfigSections: UIConfigSections = {
    preferences: [
      {
        name: 'Theme',
        key: 'theme',
        type: 'select',
        options: [
          {
            name: 'Light',
            value: 'light',
          },
          {
            name: 'Dark',
            value: 'dark',
          },
        ],
        required: false,
        description: 'Choose between light and dark layouts for the app.',
        default: 'dark',
        scope: 'client',
      },
      {
        name: 'Measurement Unit',
        key: 'measureUnit',
        type: 'select',
        options: [
          {
            name: 'Imperial',
            value: 'Imperial',
          },
          {
            name: 'Metric',
            value: 'Metric',
          },
        ],
        required: false,
        description: 'Choose between Metric  and Imperial measurement unit.',
        default: 'Metric',
        scope: 'client',
      },
      {
        name: 'Auto video & image search',
        key: 'autoMediaSearch',
        type: 'switch',
        required: false,
        description: 'Automatically search for relevant images and videos.',
        default: true,
        scope: 'client',
      },
      {
        name: 'Show weather widget',
        key: 'showWeatherWidget',
        type: 'switch',
        required: false,
        description: 'Display the weather card on the home screen.',
        default: true,
        scope: 'client',
      },
      {
        name: 'Show news widget',
        key: 'showNewsWidget',
        type: 'switch',
        required: false,
        description: 'Display the recent news card on the home screen.',
        default: true,
        scope: 'client',
      },
    ],
    personalization: [
      {
        name: 'System Instructions',
        key: 'systemInstructions',
        type: 'textarea',
        required: false,
        description: 'Add custom behavior or tone for the model.',
        placeholder:
          'e.g., "Respond in a friendly and concise tone" or "Use British English and format answers as bullet points."',
        scope: 'client',
      },
    ],
    modelProviders: [],
    search: [
      {
        name: 'SearXNG URL',
        key: 'searxngURL',
        type: 'string',
        required: false,
        description: 'The URL of your SearXNG instance',
        placeholder: 'http://localhost:4000',
        default: '',
        scope: 'server',
        env: 'SEARXNG_API_URL',
      },
      {
        name: 'News sources region',
        key: 'newsRegion',
        type: 'select',
        required: false,
        description:
          'Region of news sources. Auto detects from your location.',
        default: 'auto',
        scope: 'server',
        options: [
          { name: 'Auto (by location)', value: 'auto' },
          { name: 'America', value: 'america' },
          { name: 'EU', value: 'eu' },
          { name: 'Russia', value: 'russia' },
          { name: 'China', value: 'china' },
        ],
      },
    ],
  };

  constructor() {
    this.initialize();
  }

  private initialize() {
    this.initializeConfig();
    this.initializeFromEnv();
  }

  private saveConfig() {
    fs.writeFileSync(
      this.configPath,
      JSON.stringify(this.currentConfig, null, 2),
    );
  }

  private initializeConfig() {
    const exists = fs.existsSync(this.configPath);
    if (!exists) {
      fs.writeFileSync(
        this.configPath,
        JSON.stringify(this.currentConfig, null, 2),
      );
    } else {
      try {
        this.currentConfig = JSON.parse(
          fs.readFileSync(this.configPath, 'utf-8'),
        );
      } catch (err) {
        if (err instanceof SyntaxError) {
          console.error(
            `Error parsing config file at ${this.configPath}:`,
            err,
          );
          console.log(
            'Loading default config and overwriting the existing file.',
          );
          fs.writeFileSync(
            this.configPath,
            JSON.stringify(this.currentConfig, null, 2),
          );
          return;
        } else {
          console.log('Unknown error reading config file:', err);
        }
      }

      this.currentConfig = this.migrateConfig(this.currentConfig);
    }
  }

  private migrateConfig(config: Config): Config {
    /* TODO: Add migrations */
    return config;
  }

  /** Env-only mode: LLM from LLM_PROVIDER=ollama|timeweb, no UI for providers */
  public isEnvOnlyMode(): boolean {
    return ['ollama', 'timeweb'].includes(
      (process.env.LLM_PROVIDER ?? '').toLowerCase(),
    );
  }

  private buildEnvProvider(): ConfigModelProvider | null {
    const providerType = (process.env.LLM_PROVIDER ?? '').toLowerCase();

    if (providerType === 'ollama') {
      const defaultUrl =
        process.env.DOCKER
          ? 'http://host.docker.internal:11434'
          : 'http://localhost:11434';
      const baseURL = process.env.OLLAMA_BASE_URL ?? defaultUrl;
      const embeddingBaseURL =
        process.env.OLLAMA_EMBEDDING_BASE_URL || baseURL;
      const chatModel = process.env.LLM_CHAT_MODEL || 'llama3.2';
      const embeddingModel = process.env.LLM_EMBEDDING_MODEL || 'nomic-embed-text';

      return {
        id: 'env-ollama',
        name: 'Ollama',
        type: 'ollama',
        chatModels: [{ key: chatModel, name: chatModel }],
        embeddingModels: [{ key: embeddingModel, name: embeddingModel }],
        config: {
          baseURL,
          ...(embeddingBaseURL !== baseURL && { embeddingBaseURL }),
        },
        hash: hashObj({ baseURL, embeddingBaseURL }),
      };
    }

    if (providerType === 'timeweb') {
      const baseURL = process.env.TIMEWEB_API_BASE_URL || 'https://api.timeweb.cloud';
      const agentAccessId = process.env.TIMEWEB_AGENT_ACCESS_ID || '';
      const apiKey = process.env.TIMEWEB_API_KEY || '';
      const model = process.env.LLM_CHAT_MODEL || 'gpt-4';
      const xProxySource = process.env.TIMEWEB_X_PROXY_SOURCE;

      if (!agentAccessId || !apiKey) return null;

      return {
        id: 'env-timeweb',
        name: 'Timeweb Cloud AI',
        type: 'timeweb',
        chatModels: [{ key: model, name: model }],
        embeddingModels: [],
        config: {
          baseURL,
          agentAccessId,
          apiKey,
          model,
          xProxySource: xProxySource || undefined,
        },
        hash: hashObj({ baseURL, agentAccessId }),
      };
    }

    return null;
  }

  private initializeFromEnv() {
    const envProvider = this.buildEnvProvider();

    if (envProvider) {
      this.currentConfig.modelProviders = [envProvider];
      this.currentConfig.setupComplete = true;
      this.uiConfigSections.modelProviders = [];
    } else {
      const providerConfigSections = loadModelProvidersUIConfigSection();
      this.uiConfigSections.modelProviders = providerConfigSections;

      const newProviders: ConfigModelProvider[] = [];

      providerConfigSections.forEach((provider) => {
        const newProvider: ConfigModelProvider & { required?: string[] } = {
          id: crypto.randomUUID(),
          name: `${provider.name}`,
          type: provider.key,
          chatModels: [],
          embeddingModels: [],
          config: {},
          required: [],
          hash: '',
        };

        provider.fields.forEach((field) => {
          newProvider.config[field.key] =
            process.env[field.env!] ||
            field.default ||
            '';

          if (field.required) newProvider.required?.push(field.key);
        });

        let configured = true;

        newProvider.required?.forEach((r) => {
          if (!newProvider.config[r]) {
            configured = false;
          }
        });

        if (configured) {
          const hash = hashObj(newProvider.config);
          newProvider.hash = hash;
          delete newProvider.required;

          const exists = this.currentConfig.modelProviders.find(
            (p) => p.hash === hash,
          );

          if (!exists) {
            newProviders.push(newProvider);
          }
        }
      });

      this.currentConfig.modelProviders.push(...newProviders);
    }

    /* search section */
    this.uiConfigSections.search.forEach((f) => {
      if (f.env && !this.currentConfig.search[f.key]) {
        this.currentConfig.search[f.key] =
          process.env[f.env] ?? f.default ?? '';
      }
    });

    this.saveConfig();
  }

  public getConfig(key: string, defaultValue?: any): any {
    const nested = key.split('.');
    let obj: any = this.currentConfig;

    for (let i = 0; i < nested.length; i++) {
      const part = nested[i];
      if (obj == null) return defaultValue;

      obj = obj[part];
    }

    return obj === undefined ? defaultValue : obj;
  }

  public updateConfig(key: string, val: any) {
    const parts = key.split('.');
    if (parts.length === 0) return;

    let target: any = this.currentConfig;
    for (let i = 0; i < parts.length - 1; i++) {
      const part = parts[i];
      if (target[part] === null || typeof target[part] !== 'object') {
        target[part] = {};
      }

      target = target[part];
    }

    const finalKey = parts[parts.length - 1];
    target[finalKey] = val;

    this.saveConfig();
  }

  public addModelProvider(type: string, name: string, config: any) {
    const newModelProvider: ConfigModelProvider = {
      id: crypto.randomUUID(),
      name,
      type,
      config,
      chatModels: [],
      embeddingModels: [],
      hash: hashObj(config),
    };

    this.currentConfig.modelProviders.push(newModelProvider);
    this.saveConfig();

    return newModelProvider;
  }

  public removeModelProvider(id: string) {
    const index = this.currentConfig.modelProviders.findIndex(
      (p) => p.id === id,
    );

    if (index === -1) return;

    this.currentConfig.modelProviders =
      this.currentConfig.modelProviders.filter((p) => p.id !== id);

    this.saveConfig();
  }

  public async updateModelProvider(id: string, name: string, config: any) {
    const provider = this.currentConfig.modelProviders.find((p) => {
      return p.id === id;
    });

    if (!provider) throw new Error('Provider not found');

    provider.name = name;
    provider.config = config;

    this.saveConfig();

    return provider;
  }

  public addProviderModel(
    providerId: string,
    type: 'embedding' | 'chat',
    model: any,
  ) {
    const provider = this.currentConfig.modelProviders.find(
      (p) => p.id === providerId,
    );

    if (!provider) throw new Error('Invalid provider id');

    delete model.type;

    if (type === 'chat') {
      provider.chatModels.push(model);
    } else {
      provider.embeddingModels.push(model);
    }

    this.saveConfig();

    return model;
  }

  public removeProviderModel(
    providerId: string,
    type: 'embedding' | 'chat',
    modelKey: string,
  ) {
    const provider = this.currentConfig.modelProviders.find(
      (p) => p.id === providerId,
    );

    if (!provider) throw new Error('Invalid provider id');

    if (type === 'chat') {
      provider.chatModels = provider.chatModels.filter(
        (m) => m.key !== modelKey,
      );
    } else {
      provider.embeddingModels = provider.embeddingModels.filter(
        (m) => m.key != modelKey,
      );
    }

    this.saveConfig();
  }

  public isSetupComplete() {
    return this.currentConfig.setupComplete;
  }

  public markSetupComplete() {
    if (!this.currentConfig.setupComplete) {
      this.currentConfig.setupComplete = true;
    }

    this.saveConfig();
  }

  public getUIConfigSections(): UIConfigSections {
    return this.uiConfigSections;
  }

  public getCurrentConfig(): Config {
    return JSON.parse(JSON.stringify(this.currentConfig));
  }
}

const configManager = new ConfigManager();

export default configManager;
