/**
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseTracer, type Run } from '@langchain/core/tracers/base';
import {
  SpanStatusCode,
  context,
  trace,
  type Span as ApiSpan,
} from '@opentelemetry/api';

const TRACER_NAME = 'genkit-tracer';
const TRACER_VERSION = 'v1';

export class GenkitTracer extends BaseTracer {
  spans: Record<string, ApiSpan> = {};
  tracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);

  name = 'genkit_callback_handler' as const;

  protected persistRun(_run: Run) {
    return Promise.resolve();
  }

  getParents(run: Run) {
    const parents: Run[] = [];
    let currentRun = run;
    while (currentRun.parent_run_id) {
      const parent = this.runMap.get(currentRun.parent_run_id);
      if (parent) {
        parents.push(parent);
        currentRun = parent;
      } else {
        break;
      }
    }
    return parents;
  }

  getBreadcrumbs(run: Run) {
    const parents = this.getParents(run).reverse();
    return [...parents, run].map((parent) => `${parent.name}`).join('/');
  }

  private startSpan(run: Run) {
    let ctx;
    if (run.parent_run_id) {
      const parentCtx = this.spans[run.parent_run_id];
      ctx = trace.setSpan(context.active(), parentCtx);
    }
    const span = this.tracer.startSpan(run.name, undefined, ctx);
    if (run.inputs) {
      span.setAttribute(
        'genkit:input',
        JSON.stringify(this.maybeFlattenInput(run.inputs))
      );
    }
    span.setAttribute('langchain:path', this.getBreadcrumbs(run));
    this.spans[run.id] = span;
    return span;
  }

  private endSpan(run: Run, attributes?: Record<string, string>) {
    const span = this.spans[run.id];
    span.setAttribute('genkit:state', run.error ? 'error' : 'success');
    if (run.error) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: run.error,
      });
    }
    if (run.outputs) {
      span.setAttribute(
        'genkit:output',
        JSON.stringify(this.maybeFlattenOutput(run.outputs))
      );
    }
    if (attributes) {
      span.setAttributes(attributes);
    }
    span.end();
    return span;
  }

  onChainStart(run: Run) {
    this.startSpan(run);
  }

  onChainEnd(run: Run) {
    this.endSpan(run);
  }

  onChainError(run: Run) {
    this.endSpan(run);
  }

  onLLMStart(run: Run) {
    this.startSpan(run);
  }

  onLLMEnd(run: Run) {
    this.endSpan(run);
  }

  onLLMError(run: Run) {
    this.endSpan(run);
  }

  onToolStart(run: Run) {
    this.startSpan(run);
  }

  onToolEnd(run: Run) {
    this.endSpan(run);
  }

  onToolError(run: Run) {
    this.endSpan(run);
  }

  onRetrieverStart(run: Run) {
    this.startSpan(run);
  }

  onRetrieverEnd(run: Run) {
    this.endSpan(run);
  }

  onRetrieverError(run: Run) {
    this.endSpan(run);
  }

  onAgentAction(run: Run) {
    this.startSpan(run);
    this.endSpan(run);
  }

  private maybeFlattenInput(input: any) {
    if (
      input &&
      input.hasOwnProperty('input') &&
      Object.keys(input).length === 1
    ) {
      return input.input;
    }
    return input;
  }

  private maybeFlattenOutput(output: any) {
    if (
      output &&
      output.hasOwnProperty('output') &&
      Object.keys(output).length === 1
    ) {
      return output.output;
    }
    return output;
  }
}
