import { ConfigFileSchema } from '@alwaysai/config-nodejs';
import Ajv, { JSONSchemaType } from 'ajv';
import { join, posix } from 'path';
import { SystemId } from '../constants';
import { ALWAYSAI_SYSTEM_ID } from '../environment';
import {
  ALWAYSAI_CONFIG_FILE_NAME,
  LOCAL_AAI_CFG_DIR,
  REMOTE_AAI_CFG_DIR_LINUX
} from '../paths';
import { SshSpawner, logger } from '../util';

export const SYSTEM_IDS = Object.keys(SystemId) as SystemId[];

const path = join(LOCAL_AAI_CFG_DIR, ALWAYSAI_CONFIG_FILE_NAME);

export type AaiConfig = {
  systemId: SystemId;
};

export const aaiConfigSchema: JSONSchemaType<AaiConfig> = {
  type: 'object',
  properties: {
    systemId: {
      type: 'string',
      enum: SYSTEM_IDS
    }
  },
  required: ['systemId']
};

const ajv = new Ajv();

const validateAaiConfig = ajv.compile(aaiConfigSchema);
function AaiConfigFile(baseDir?: string) {
  const filePath = join(baseDir ?? path);
  const configFile = ConfigFileSchema<AaiConfig>({
    path: filePath,
    validateFunction: validateAaiConfig,
    initialValue: { systemId: 'production' }
  });
  return configFile;
}

type AaiConfigFileReturnType = ReturnType<typeof AaiConfigFile>;
abstract class AaiCfg {
  aaiConfigFile: AaiConfigFileReturnType;
  baseDir: string;
  fullPath: string;

  constructor(baseDir: string) {
    this.baseDir = baseDir;
  }

  public writeAaiCfgFile(): void {
    // intentionally empty
  }

  public readAaiCfgFile(): void {
    // intentionally empty
  }

  public getFileName(): string {
    return ALWAYSAI_CONFIG_FILE_NAME;
  }
}

export class LocalAaiCfg extends AaiCfg {
  aaiConfigFile: AaiConfigFileReturnType;
  baseDir: string;

  constructor(baseDir: string = LOCAL_AAI_CFG_DIR) {
    super(baseDir);
    this.fullPath = join(this.baseDir, this.getFileName());
    this.aaiConfigFile = AaiConfigFile(this.fullPath);
  }

  public async getValidationErrors() {
    return validateAaiConfig.errors;
  }

  public async writeAaiCfgFile(): Promise<void> {
    let contents: AaiConfig = { systemId: getSystemId() };

    if (this.aaiConfigFile.exists()) {
      try {
        // NOTE: readAaiCfgFile will do a validation
        const origParsed = await this.readAaiCfgFile();
        contents = { ...origParsed, ...contents };
      } catch (err) {
        logger.error(
          `${this.getFileName()} is invalid:\n${JSON.stringify(
            this.getValidationErrors(),
            null,
            2
          )}`
        );
        this.aaiConfigFile.remove();
      }
    }
    this.aaiConfigFile.write(contents);
  }

  public async readAaiCfgFile(): Promise<AaiConfig> {
    const parsedContents = this.aaiConfigFile.read();
    return parsedContents;
  }

  public getFileName(): string {
    return super.getFileName();
  }
}

export class RemoteAaiCfg extends AaiCfg {
  aaiConfigFile: AaiConfigFileReturnType;
  baseDir: string;
  fullPath: string;
  spawner: SshSpawner;

  constructor(
    targetHostName: string,
    baseDir: string = REMOTE_AAI_CFG_DIR_LINUX
  ) {
    super(baseDir);
    this.fullPath = posix.join(this.baseDir, this.getFileName());
    this.spawner = SshSpawner({
      targetHostname: targetHostName,
      targetPath: this.baseDir
    });
    this.aaiConfigFile = AaiConfigFile(this.fullPath);
  }
  s;

  private validate(parsedContents: any) {
    return this.aaiConfigFile.validate(parsedContents);
  }

  public async writeAaiCfgFile(): Promise<void> {
    let contents = { systemId: getSystemId() };

    if (await this.spawner.exists(this.getFileName())) {
      logger.debug(`${this.getFileName()} already exists, updating file.`);
      try {
        // NOTE: readAaiCfgFile will do a validation
        const origParsed = await this.readAaiCfgFile();
        contents = { ...origParsed, ...contents };
      } catch (e) {
        logger.error(
          `${this.getFileName()} is invalid:\n${JSON.stringify(
            this.getValidationErrors(),
            null,
            2
          )}`
        );
        await this.spawner.rimraf(this.getFileName());
      }
    }
    await this.spawner.mkdirp();
    await this.spawner.writeFile(
      this.getFileName(),
      JSON.stringify(contents, null, 2)
    );
  }

  public async getValidationErrors() {
    return validateAaiConfig.errors;
  }

  public async readAaiCfgFile(): Promise<AaiConfig> {
    const origContents = await this.spawner.readFile(this.getFileName());
    const origParsed = JSON.parse(origContents);
    if (!this.validate(origParsed)) {
      throw new Error(
        `Validation of ${this.getFileName()} failed:\n${JSON.stringify(
          this.getValidationErrors(),
          null,
          2
        )}!`
      );
    }
    return origParsed;
  }
}

/*===================================================================
                      System ID Usage
===================================================================*/

export function setSystemId(systemId: SystemId) {
  const localAaiConfigFile = new LocalAaiCfg();
  // TODO: replace with pure wrapper functionality
  return localAaiConfigFile.aaiConfigFile.update((json) => {
    json.systemId = systemId;
  });
}

export function getSystemId() {
  const localAaiConfigFile = new LocalAaiCfg();
  // TODO: can catch validation errors here and display them
  const maybeConfig = localAaiConfigFile.aaiConfigFile.readIfExists();
  if (ALWAYSAI_SYSTEM_ID) {
    // When the env var is set return the overridden value without updating the config file.
    // This prevents one-off commands from having side effects.
    if (Object.keys(SystemId).includes(ALWAYSAI_SYSTEM_ID)) {
      if (!maybeConfig?.systemId) {
        setSystemId(ALWAYSAI_SYSTEM_ID as SystemId);
      }
      return ALWAYSAI_SYSTEM_ID as SystemId;
    }
    throw new Error(`Invalid ALWAYSAI_SYSTEM_ID: ${ALWAYSAI_SYSTEM_ID}`);
  }
  // TODO: replace with pure wrapper functionality

  if (!maybeConfig?.systemId) {
    setSystemId('production');
  }
  const systemId = maybeConfig?.systemId ?? 'production';
  return systemId as SystemId;
}
