import { join } from 'path';
import Ajv, { JSONSchemaType } from 'ajv';

import * as chalk from 'chalk';

import { CliTerseError, CLI_TERSE_ERROR } from '@alwaysai/alwayscli';

import { TargetProtocol } from './target-protocol';
import { ALWAYSAI_CLI_EXECUTABLE_NAME } from '../../constants';
import {
  DockerSpawner,
  SshSpawner,
  JsSpawner,
  Spawner,
  SshDockerSpawner,
  logger,
  stringifyError
} from '../../util';
import {
  TargetHardware,
  TARGET_HARDWARE_OPTIONS
} from './get-target-hardware-type';
import {
  ConfigFileSchema,
  ConfigFileSchemaReturnType
} from '@alwaysai/config-nodejs';
import { TARGET_JSON_FILE_NAME } from '../../paths';

export interface SshDockerTargetConfig {
  targetProtocol: 'ssh+docker:';
  targetHostname: string;
  targetPath: string;
  dockerImageId: string;
  targetHardware: TargetHardware;
  deviceId: string;
}

export interface DockerTargetConfig {
  targetProtocol: 'docker:';
  dockerImageId: string;
  targetHardware: TargetHardware;
}

export interface NativeTargetConfig {
  targetProtocol: 'native:';
}

export type TargetConfig =
  | SshDockerTargetConfig
  | DockerTargetConfig
  | NativeTargetConfig;

export const targetConfigSchema: JSONSchemaType<TargetConfig> = {
  anyOf: [
    {
      type: 'object',
      properties: {
        targetProtocol: {
          type: 'string',
          const: 'ssh+docker:'
        },
        targetHostname: {
          type: 'string'
        },
        targetPath: {
          type: 'string'
        },
        dockerImageId: {
          type: 'string'
        },
        targetHardware: {
          type: 'string',
          enum: TARGET_HARDWARE_OPTIONS
        },
        deviceId: {
          type: 'string'
        }
      },
      required: [
        'targetProtocol',
        'targetHostname',
        'targetPath',
        'dockerImageId',
        'targetHardware',
        'deviceId'
      ]
    },
    {
      type: 'object',
      properties: {
        targetProtocol: {
          type: 'string',
          const: 'docker:'
        },
        dockerImageId: {
          type: 'string'
        },
        targetHardware: {
          type: 'string',
          enum: TARGET_HARDWARE_OPTIONS
        }
      },
      required: ['targetProtocol', 'dockerImageId', 'targetHardware']
    },
    {
      type: 'object',
      properties: {
        targetProtocol: {
          type: 'string',
          const: 'native:'
        }
      },
      required: ['targetProtocol']
    }
  ]
};

const ajv = new Ajv();

const validateFunction = ajv.compile(targetConfigSchema);

const DID_YOU_RUN_APP_CONFIGURE = `Did you run "${ALWAYSAI_CLI_EXECUTABLE_NAME} app configure"?`;

const ENOENT = {
  message: `${TARGET_JSON_FILE_NAME} not found. ${DID_YOU_RUN_APP_CONFIGURE}`,
  code: CLI_TERSE_ERROR
};

export interface TargetJsonFileReturnType
  extends ConfigFileSchemaReturnType<TargetConfig> {
  name: string;
  readContainerSpawner: (opts?: {
    ignoreTargetHardware?: boolean;
    volumes?: string[];
    env_vars?: string[];
  }) => Spawner;
  readHostSpawner: () => Spawner;
  readTargetProtocolSafe: () =>
    | 'ssh+docker:'
    | 'docker:'
    | 'native:'
    | undefined;
  readFieldSafe: (props: { name: string }) => string | undefined;
  describe: () => string | undefined;
}

export function TargetJsonFile(cwd = process.cwd()): TargetJsonFileReturnType {
  const filePath = join(cwd, TARGET_JSON_FILE_NAME);
  const configFile = ConfigFileSchema({
    path: filePath,
    validateFunction,
    ENOENT
  });

  return {
    ...configFile,
    name: TARGET_JSON_FILE_NAME,
    readContainerSpawner,
    readHostSpawner,
    readTargetProtocolSafe,
    readFieldSafe,
    describe
  };

  function describe() {
    const config = configFile.readIfExists();
    if (!config) {
      return `Target configuration file "${TARGET_JSON_FILE_NAME}" not found`;
    }
    const docker = chalk.bold('docker');
    switch (config.targetProtocol) {
      case 'native:': {
        return `Target: Native alwaysAI Python on this host`;
      }
      case 'docker:': {
        return `Target: ${docker} container on this host`;
      }
      case 'ssh+docker:': {
        const hostname = chalk.bold(config.targetHostname);
        const path = chalk.bold(config.targetPath);
        return `Target: ${docker} container on ${hostname}, path ${path}`;
      }
      default:
        throw new Error('Unsupported protocol');
    }
  }

  function readContainerSpawner(opts?: {
    ignoreTargetHardware?: boolean;
    volumes?: string[];
    env_vars?: string[];
  }) {
    const targetJson = configFile.read();
    const volumes = opts?.volumes;
    const env_vars = opts?.env_vars;
    switch (targetJson.targetProtocol) {
      case 'ssh+docker:': {
        const { targetHostname, targetPath, dockerImageId, targetHardware } =
          targetJson;
        return SshDockerSpawner({
          dockerImageId,
          targetPath,
          targetHostname,
          targetHardware,
          volumes,
          env_vars
        });
      }

      case 'docker:': {
        const { dockerImageId } = targetJson;
        const targetHardware =
          opts && opts.ignoreTargetHardware
            ? undefined
            : targetJson.targetHardware;
        return DockerSpawner({
          dockerImageId,
          targetHardware,
          volumes,
          env_vars
        });
      }

      case 'native:':
      default:
        throw new CliTerseError('Unsupported protocol');
    }
  }

  function readHostSpawner() {
    const targetJson = configFile.read();
    switch (targetJson.targetProtocol) {
      case 'ssh+docker:': {
        const { targetPath, targetHostname } = targetJson;
        return SshSpawner({
          targetHostname,
          targetPath
        });
      }

      case 'docker:':
      case 'native:': {
        return JsSpawner();
      }

      default:
        throw new CliTerseError('Unsupported protocol');
    }
  }

  function readTargetProtocolSafe() {
    try {
      const targetJson = configFile.readIfExists();
      if (!targetJson) {
        return undefined;
      }
      return targetJson.targetProtocol;
    } catch (err) {
      configFile.remove();
      return undefined;
    }
  }

  function readFieldSafe(props: { name: string }) {
    try {
      const targetJson = configFile.readIfExists();
      if (!targetJson) {
        return undefined;
      }
      switch (targetJson.targetProtocol) {
        case 'ssh+docker:': {
          switch (props.name) {
            case 'targetProtocol':
              return TargetProtocol[targetJson.targetProtocol];
            case 'targetHostname':
              return targetJson.targetHostname;
            case 'targetPath':
              return targetJson.targetPath;
            case 'dockerImageId':
              return targetJson.dockerImageId;
            case 'targetHardware':
              return targetJson.targetHardware;
            case 'deviceId':
              return targetJson.deviceId;
            default:
              return undefined;
          }
        }

        case 'docker:': {
          switch (props.name) {
            case 'targetProtocol':
              return TargetProtocol[targetJson.targetProtocol];
            case 'dockerImageId':
              return targetJson.dockerImageId;
            case 'targetHardware':
              return targetJson.targetHardware;
            default:
              return undefined;
          }
        }

        case 'native:': {
          switch (props.name) {
            case 'targetProtocol':
              return TargetProtocol[targetJson.targetProtocol];
            default:
              return undefined;
          }
        }

        default:
          throw new CliTerseError('Unsupported protocol');
      }
    } catch (err) {
      logger.error(stringifyError(err));
      configFile.remove();
      return undefined;
    }
  }
}
