// StudioFlowRunDriver — adapter that bridges the editor
// package's FlowRunDriver interface to Studio's HubService.
//
// Lives in Studio (not in the editor package) so the package
// stays host-agnostic. The driver maps:
//
//   - editor.runFlow(...) -> HubService.runSavedFlow
//   - editor.events()     -> HubService.streamEvents filtered
//                            to step.* events and reshaped
//                            into the editor's FlowRunEvent
//                            value classes.

import 'dart:async';
import 'dart:typed_data';

import 'package:chain_studio_flow_editor/chain_studio_flow_editor.dart' as editor;

import 'hub.dart';

class StudioFlowRunDriver implements editor.FlowRunDriver {
  StudioFlowRunDriver();

  @override
  Future<Map<String, editor.FlowOutputValue>> runFlow({
    required String flowName,
    required Map<String, String> textInputs,
    required Map<String, Uint8List> fileInputs,
    required Map<String, String> fileMimes,
  }) async {
    final outputs = await HubService.instance.runSavedFlow(
      name: flowName,
      textInputs: textInputs,
      fileInputs: fileInputs,
      fileMimeTypes: fileMimes,
    );
    final mapped = <String, editor.FlowOutputValue>{};
    for (final entry in outputs.entries) {
      mapped[entry.key] = _convertOutput(entry.value);
    }
    return mapped;
  }

  @override
  Future<editor.ModuleSpec?> moduleInfo(String capability) async {
    try {
      final detail = await HubService.instance.moduleInfo(capability);
      return editor.ModuleSpec(
        capability: capability,
        inputs: detail.inputs
            .map(
              (f) => editor.ModuleField(
                name: f.name,
                type: f.type,
                description: f.description,
              ),
            )
            .toList(),
        outputs: detail.outputs
            .map(
              (f) => editor.ModuleField(
                name: f.name,
                type: f.type,
                description: f.description,
              ),
            )
            .toList(),
      );
    } catch (_) {
      // Module not installed locally, or transient hub error.
      // Returning null lets the editor fall back to YAML-
      // derived ports instead of failing the whole canvas.
      return null;
    }
  }

  @override
  Stream<editor.FlowRunEvent> events() {
    return HubService.instance
        .streamEvents(
          backfill: 0,
          types: const [
            'step.started',
            'step.completed',
            'step.failed',
            'step.awaiting_approval',
            'step.approved',
            'step.rejected',
          ],
        )
        .map(_convertEvent)
        .where((e) => e != null)
        .cast<editor.FlowRunEvent>();
  }

  editor.FlowRunEvent? _convertEvent(AuditEvent e) {
    final flowName = e.flowName;
    final stepId = e.stepId;
    if (flowName == null || flowName.isEmpty) return null;
    if (stepId == null || stepId.isEmpty) return null;
    switch (e.type) {
      case 'step.started':
      case 'step.approved':
        return editor.StepStarted(flowName: flowName, stepId: stepId);
      case 'step.completed':
        return editor.StepCompleted(
          flowName: flowName,
          stepId: stepId,
          durationMs: e.durationMs ?? 0,
        );
      case 'step.failed':
      case 'step.rejected':
        return editor.StepFailed(
          flowName: flowName,
          stepId: stepId,
          error: e.error ?? '',
        );
      case 'step.awaiting_approval':
        return editor.StepAwaitingApproval(flowName: flowName, stepId: stepId);
    }
    return null;
  }

  @override
  Future<String?> pendingApprovalIdForStep({
    required String flowName,
    required String stepId,
  }) async {
    try {
      final pending = await HubService.instance.pendingApprovals();
      // Match the newest approval for this (flow, step). The hub
      // creates one approval row per step invocation; if multiple
      // are pending we surface the most recent (operator can still
      // navigate to the standalone Approvals page for the others).
      final match = pending
          .where((p) => p.flowName == flowName && p.stepId == stepId)
          .toList()
        ..sort((a, b) => b.createdAt.compareTo(a.createdAt));
      return match.isEmpty ? null : match.first.id;
    } catch (_) {
      return null;
    }
  }

  @override
  Future<void> approveApproval({
    required String approvalId,
    required String reviewer,
  }) =>
      HubService.instance.approve(approvalId, reviewer);

  @override
  Future<void> rejectApproval({
    required String approvalId,
    required String reviewer,
    required String reason,
  }) =>
      HubService.instance.reject(approvalId, reviewer, reason);

  /// Studio's FlowOutput hierarchy is gRPC-shaped (one class
  /// per payload variant); the editor's is host-agnostic
  /// (text / json / bytes only). File outputs degrade to
  /// JSON containing the URI string — the editor's run-tab
  /// renderer is read-only so the operator can still see
  /// where the file went; Studio's richer ChainFlowOutput
  /// widget keeps file-open affordances available elsewhere.
  editor.FlowOutputValue _convertOutput(FlowOutput out) {
    return switch (out) {
      FlowOutputText() => editor.FlowOutputText(out.text),
      FlowOutputJson() => editor.FlowOutputJson(out.pretty),
      FlowOutputBytes() => editor.FlowOutputBytes(out.bytes, out.mimeType),
      FlowOutputFile() => editor.FlowOutputJson({
        'file_uri': out.uri,
        'mime_type': out.mimeType,
      }),
      FlowOutputUnknown() => const editor.FlowOutputText(''),
    };
  }
}
