import { IProjectRunnerOptions } from '../runtime/node/InteropInterfaces.js'
import { Kind as HttpProjectKind, HttpProject, IHttpProject } from '../models/HttpProject.js'
import { StoreSdk } from '../sdk/StoreSdkNode.js'
import { ProjectSerialRunner } from '../runtime/node/ProjectSerialRunner.js'
import { ProjectParallelRunner } from '../runtime/node/ProjectParallelRunner.js'
import Proxy, { IProxyResult } from './Proxy.js'
import { IProjectExecutionLog } from '../models/ProjectExecution.js'
import { Exception } from '../exceptions/exception.js'

interface ProxyInit {
  kind: typeof HttpProjectKind
  /**
   * Runner options.
   */
  options: IProjectRunnerOptions
  /**
   * A signal to be sent to the proxy bindings to abort the request on demand.
   */
  signal?: AbortSignal | null
}

export interface IHttpProjectProxyInit extends ProxyInit {
  project: IHttpProject
}

export interface IHttpProjectStoreProxyInit extends ProxyInit {
  kind: typeof HttpProjectKind
  /**
   * Project key
   */
  pid: string
  /**
   * The parent organization key.
   */
  organization: string
}

/**
 * Runs requests from an HTTP project read from the store.
 */
export default class HttpProjectProxy extends Proxy {
  project?: HttpProject
  options?: IProjectRunnerOptions

  protected async configureStore(init: IHttpProjectStoreProxyInit, token: string, baseUri: string): Promise<void> {
    const { pid, options, organization } = init
    const INVALID_REQUEST = 'Invalid request'
    const code = 'E_INVALID_REQUEST'
    if (!pid) {
      throw new Exception(INVALID_REQUEST, {
        help: 'The "pid" parameter is required.',
        status: 400,
        code,
      })
    }
    if (!organization) {
      throw new Exception(INVALID_REQUEST, {
        help: 'The "organization" parameter is required.',
        status: 400,
        code,
      })
    }
    if (!options) {
      throw new Exception(INVALID_REQUEST, {
        help: 'The "options" parameter is required.',
        status: 400,
        code,
      })
    }
    if (!token) {
      throw new Exception(INVALID_REQUEST, {
        help: 'Set the authentication credentials.',
        status: 400,
        code,
      })
    }
    if (!baseUri) {
      throw new Exception(INVALID_REQUEST, {
        help: 'The store uri is missing.',
        status: 400,
        code,
      })
    }
    if (!baseUri.startsWith('http')) {
      throw new Exception(INVALID_REQUEST, {
        help: 'The store uri is invalid.',
        status: 400,
        code,
      })
    }
    const sdk = new StoreSdk(baseUri)
    sdk.token = token
    let project: IHttpProject
    try {
      const data = await sdk.file.readMedia(organization, pid)
      project = data.media as IHttpProject
    } catch (cause) {
      const e = cause as Error
      throw new Exception(e.message, { status: 400 })
    }
    if (project.key !== pid) {
      throw new Exception(`Unable to read the project.`, { status: 500 })
    }
    this.options = options
    this.project = new HttpProject(project)
  }

  protected async configureProject(init: IHttpProjectProxyInit): Promise<void> {
    const { project, options } = init
    const INVALID_REQUEST = 'Invalid request'
    const code = 'E_INVALID_REQUEST'
    if (!options) {
      throw new Exception(INVALID_REQUEST, {
        help: 'The "options" parameter is required.',
        status: 400,
        code,
      })
    }
    if (!project) {
      throw new Exception(INVALID_REQUEST, {
        help: 'The "project" parameter is required.',
        status: 400,
        code,
      })
    }
    this.options = options
    this.project = new HttpProject(project)
  }

  async configure(
    init: IHttpProjectProxyInit | IHttpProjectStoreProxyInit,
    token?: string,
    baseUri?: string
  ): Promise<void> {
    const typedStore = init as IHttpProjectStoreProxyInit
    if (typeof typedStore.pid === 'string') {
      return this.configureStore(typedStore, token as string, baseUri as string)
    }
    const typedProject = init as IHttpProjectProxyInit
    return this.configureProject(typedProject)
  }

  async execute(): Promise<IProxyResult<IProjectExecutionLog>> {
    const project = this.project as HttpProject
    const opts = this.options as IProjectRunnerOptions
    let factory: ProjectParallelRunner | ProjectSerialRunner
    if (opts.parallel) {
      factory = new ProjectParallelRunner(project, opts)
    } else {
      factory = new ProjectSerialRunner()
      factory.configure(project, opts)
    }

    function unhandledRejection(): void {
      //
    }
    // the executing library handles all related errors it need.
    // However, when executing a request to an unknown host Node process may
    // throw unhandled error event when the error is properly reported by the
    // application. This suppresses these errors.
    // Note, uncomment this line for debug.
    process.on('unhandledRejection', unhandledRejection)
    const report = await factory.execute()
    process.off('unhandledRejection', unhandledRejection)
    return {
      result: report,
    }
  }
}
