import {
  MetricCounter,
  MetricGauge,
  MetricHistogram,
  MetricMeter,
  MetricMeterWithComposedTags,
  MetricTags,
  NumericMetricValueType,
} from '@temporalio/common';
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
import { proxySinks, Sink, Sinks } from './sinks';
import { workflowInfo } from './workflow';
import { assertInWorkflowContext } from './global-attributes';

class WorkflowMetricMeterImpl implements MetricMeter {
  constructor() {}

  createCounter(name: string, unit?: string, description?: string): MetricCounter {
    assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
    return new WorkflowMetricCounter(name, unit, description);
  }

  createHistogram(
    name: string,
    valueType: NumericMetricValueType = 'int',
    unit?: string,
    description?: string
  ): MetricHistogram {
    assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
    return new WorkflowMetricHistogram(name, valueType, unit, description);
  }

  createGauge(
    name: string,
    valueType: NumericMetricValueType = 'int',
    unit?: string,
    description?: string
  ): MetricGauge {
    assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
    return new WorkflowMetricGauge(name, valueType, unit, description);
  }

  withTags(_tags: MetricTags): MetricMeter {
    assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
    // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
    throw new Error(`withTags is not supported directly on WorkflowMetricMeter`);
  }
}

class WorkflowMetricCounter implements MetricCounter {
  public readonly kind = 'counter';
  public readonly valueType = 'int';

  constructor(
    public readonly name: string,
    public readonly unit: string | undefined,
    public readonly description: string | undefined
  ) {}

  add(value: number, extraTags: MetricTags = {}): void {
    if (value < 0) {
      throw new Error(`MetricCounter value must be non-negative (got ${value})`);
    }
    if (!workflowInfo().unsafe.isReplaying) {
      metricSink.addMetricCounterValue(this.name, this.unit, this.description, value, extraTags);
    }
  }

  withTags(_tags: MetricTags): MetricCounter {
    // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
    throw new Error(`withTags is not supported directly on WorkflowMetricCounter`);
  }
}

class WorkflowMetricHistogram implements MetricHistogram {
  public readonly kind = 'histogram';

  constructor(
    public readonly name: string,
    public readonly valueType: NumericMetricValueType,
    public readonly unit: string | undefined,
    public readonly description: string | undefined
  ) {}

  record(value: number, extraTags: MetricTags = {}): void {
    if (value < 0) {
      throw new Error(`MetricHistogram value must be non-negative (got ${value})`);
    }
    if (!workflowInfo().unsafe.isReplaying) {
      metricSink.recordMetricHistogramValue(this.name, this.valueType, this.unit, this.description, value, extraTags);
    }
  }

  withTags(_tags: MetricTags): MetricHistogram {
    // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
    throw new Error(`withTags is not supported directly on WorkflowMetricHistogram`);
  }
}

class WorkflowMetricGauge implements MetricGauge {
  public readonly kind = 'gauge';

  constructor(
    public readonly name: string,
    public readonly valueType: NumericMetricValueType,
    public readonly unit: string | undefined,
    public readonly description: string | undefined
  ) {}

  set(value: number, tags?: MetricTags): void {
    if (value < 0) {
      throw new Error(`MetricGauge value must be non-negative (got ${value})`);
    }
    if (!workflowInfo().unsafe.isReplaying) {
      metricSink.setMetricGaugeValue(this.name, this.valueType, this.unit, this.description, value, tags ?? {});
    }
  }

  withTags(_tags: MetricTags): MetricGauge {
    // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
    throw new Error(`withTags is not supported directly on WorkflowMetricGauge`);
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////

// Note: given that forwarding metrics outside of the sanbox can be quite chatty and add non
// negligeable overhead, we eagerly check for `isReplaying` and completely skip doing sink
// calls if we are replaying.
const metricSink = proxySinks<MetricSinks>().__temporal_metrics;

/**
 * Sink interface for forwarding metrics from the Workflow sandbox to the Worker.
 *
 * These sink functions are not intended to be called directly from workflow code; instead,
 * developers should use the `metricMeter` object exposed to workflow code by the SDK, which
 * provides an API that is easier to work with.
 *
 * This sink interface is also not meant to be implemented by user.
 *
 * @hidden
 * @internal Users should not implement this interface, nor use it directly. Use `metricMeter` instead.
 */
export interface MetricSinks extends Sinks {
  __temporal_metrics: WorkflowMetricMeter;
}

/**
 * @hidden
 * @internal Users should not implement this interface, nor use it directly. Use `metricMeter` instead.
 */
export interface WorkflowMetricMeter extends Sink {
  addMetricCounterValue(
    metricName: string,
    unit: string | undefined,
    description: string | undefined,
    value: number,
    attrs: MetricTags
  ): void;

  recordMetricHistogramValue(
    metricName: string,
    valueType: NumericMetricValueType,
    unit: string | undefined,
    description: string | undefined,
    value: number,
    attrs: MetricTags
  ): void;

  setMetricGaugeValue(
    metricName: string,
    valueType: NumericMetricValueType,
    unit: string | undefined,
    description: string | undefined,
    value: number,
    attrs: MetricTags
  ): void;
}

/**
 * A MetricMeter that can be used to emit metrics from within a Workflow.
 *
 * @experimental The Metric API is an experimental feature and may be subject to change.
 */
export const metricMeter: MetricMeter = MetricMeterWithComposedTags.compose(
  new WorkflowMetricMeterImpl(),
  () => {
    const activator = assertInWorkflowContext('Workflow.metricMeter may only be used from workflow context.');
    const getMetricTags = composeInterceptors(activator.interceptors.outbound, 'getMetricTags', (a) => a);

    const info = activator.info;
    return getMetricTags({
      // namespace and taskQueue will be added by the Worker
      workflowType: info.workflowType,
    });
  },
  true
);
