// SpeechBridge.m
#import "SpeechBridge.h"
#import <React/RCTLog.h>
#import <React/RCTConvert.h>
#import <AudioToolbox/AudioToolbox.h>

// Import your Swift classes (names as in your project)
#import <DaVoiceTTS/DaVoiceTTS-Swift.h>   // DaVoiceTTS + STT live here in your setup

#import <AVFAudio/AVFAudio.h>

static NSData *SB_Base64Decode(NSString *b64) {
  if (!b64 || (id)b64 == [NSNull null]) return nil;
  return [[NSData alloc] initWithBase64EncodedString:b64 options:0];
}

// Make a mono Float32 AVAudioPCMBuffer from raw PCM payload (i16 or f32).
// We accept either interleaved or non-interleaved input and mixdown to mono
// (DaVoiceTTS.playBuffer will resample / normalize as needed).
static AVAudioPCMBuffer *SB_MakeMonoF32Buffer(NSData *raw,
                                              double sampleRate,
                                              NSString *fmt,      // "i16" | "f32"
                                              NSInteger channels, // >= 1
                                              BOOL interleaved)
{
  if (!raw || raw.length == 0 || channels < 1) return nil;

  // Target: mono float32, non-interleaved
  AVAudioFormat *dstFmt = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:sampleRate channels:1];
  NSUInteger frameCount = 0;

  if ([fmt.lowercaseString isEqualToString:@"i16"]) {
    // Each sample is 2 bytes
    const NSUInteger bytesPerSample = 2;
    if (raw.length % (bytesPerSample * channels) != 0) return nil;
    frameCount = raw.length / (bytesPerSample * channels);

    AVAudioPCMBuffer *buf = [[AVAudioPCMBuffer alloc] initWithPCMFormat:dstFmt frameCapacity:(AVAudioFrameCount)frameCount];
    buf.frameLength = (AVAudioFrameCount)frameCount;

    const int16_t *in = (const int16_t *)raw.bytes;
    float *out = buf.floatChannelData[0];
    const float scale = 1.0f / 32768.0f;

    if (interleaved || channels == 1) {
      // Interleaved: [c0,c1,.., c0,c1,..]
      for (NSUInteger f = 0; f < frameCount; ++f) {
        int64_t acc = 0;
        for (NSInteger ch = 0; ch < channels; ++ch) {
          acc += in[f*channels + ch];
        }
        out[f] = (float)(acc / (double)channels) * scale;
      }
    } else {
      // Non-interleaved planar i16 (rare): [all c0][all c1]…
      const NSUInteger planeLen = frameCount;
      for (NSUInteger f = 0; f < frameCount; ++f) {
        int64_t acc = 0;
        for (NSInteger ch = 0; ch < channels; ++ch) {
          acc += in[ch*planeLen + f];
        }
        out[f] = (float)(acc / (double)channels) * scale;
      }
    }
    return buf;
  }

  // f32 path
  if (![fmt.lowercaseString isEqualToString:@"f32"]) return nil;
  const NSUInteger bytesPerSample = 4;
  if (raw.length % (bytesPerSample * channels) != 0) return nil;
  frameCount = raw.length / (bytesPerSample * channels);

  AVAudioPCMBuffer *buf = [[AVAudioPCMBuffer alloc] initWithPCMFormat:dstFmt frameCapacity:(AVAudioFrameCount)frameCount];
  buf.frameLength = (AVAudioFrameCount)frameCount;

  const float *in = (const float *)raw.bytes;
  float *out = buf.floatChannelData[0];

  if (interleaved || channels == 1) {
    for (NSUInteger f = 0; f < frameCount; ++f) {
      double acc = 0.0;
      for (NSInteger ch = 0; ch < channels; ++ch) {
        acc += in[f*channels + ch];
      }
      out[f] = (float)(acc / (double)channels);
    }
  } else {
    // Planar f32: [all c0][all c1]…
    const NSUInteger planeLen = frameCount;
    for (NSUInteger f = 0; f < frameCount; ++f) {
      double acc = 0.0;
      for (NSInteger ch = 0; ch < channels; ++ch) {
        acc += in[ch*planeLen + f];
      }
      out[f] = (float)(acc / (double)channels);
    }
  }
  return buf;
}

@interface SpeechBridge () <STTDelegate>
@property (nonatomic, strong, nullable) STT *stt;
@property (nonatomic, strong, nullable) DaVoiceTTS *tts;
@property (nonatomic, assign) BOOL hasListeners;
@property (atomic, assign) BOOL initializing;
@property (atomic, assign) BOOL initialized;

// used only to gate TTS init until STT engine is “hot”
@property (atomic, assign) BOOL sttEngineHot;
@end

@implementation SpeechBridge

RCT_EXPORT_MODULE(SpeechBridge)

// We emit the union of STT + TTS events
- (NSArray<NSString *> *)supportedEvents
{
  return @[
    // STT events
    @"onSpeechResults",
    @"onSpeechStart",
    @"onSpeechPartialResults",
    @"onSpeechError",
    @"onSpeechEnd",
    @"onSpeechRecognized",
    @"onSpeechVolumeChanged",
    // TTS event
    @"onFinishedSpeaking"
  ];
}

+ (BOOL)requiresMainQueueSetup { return YES; }
- (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }
- (void)startObserving { self.hasListeners = YES; }
- (void)stopObserving  { self.hasListeners = NO;  }

- (void)dealloc
{
  // destroy in the safe order: TTS → STT
  if (_tts) { [_tts destroy]; _tts = nil; }
  if (_stt) { [_stt destroySpeech:nil]; _stt = nil; }
}

#pragma mark - STTDelegate (forward all events)

- (void)stt:(STT *)stt didEmitEvent:(NSString *)name body:(NSDictionary *)body
{
  // Use the first onSpeechStart as the “engine hot” latch.
  if ([name isEqualToString:@"onSpeechStart"] && !self.sttEngineHot) {
    self.sttEngineHot = YES;
  }
  if (self.hasListeners) {
    [self sendEventWithName:name body:body ?: @{}];
  }
}

#pragma mark - Helpers

- (NSURL *)resolveLocalURLFromPathOrURL:(NSString *)pathOrURL
{
  if (!pathOrURL || (id)pathOrURL == [NSNull null] || pathOrURL.length == 0) return nil;
  // ✅ Handle http(s): download to tmp and return file URL
  if ([pathOrURL hasPrefix:@"http://"] || [pathOrURL hasPrefix:@"https://"]) {
    NSURL *remoteURL = [NSURL URLWithString:pathOrURL];
    if (!remoteURL) return nil;

    NSData *data = [NSData dataWithContentsOfURL:remoteURL];
    if (!data) return nil;

    // keep extension if possible
    NSString *ext = remoteURL.pathExtension.length ? remoteURL.pathExtension : @"bin";
    NSString *tempName = [NSString stringWithFormat:@"rn_asset_%f.%@", [[NSDate date] timeIntervalSince1970], ext];
    NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempName];

    [[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];
    if (![data writeToFile:tempPath atomically:YES]) return nil;

    return [NSURL fileURLWithPath:tempPath];
  }

  // Already a file URL
  if ([pathOrURL hasPrefix:@"file://"]) {
    return [NSURL URLWithString:pathOrURL];
  }

  // RN bundled asset path: asset:/xxx  -> copy from bundle to tmp so we get a file URL
  if ([pathOrURL hasPrefix:@"asset:/"]) {
    NSString *assetName = [pathOrURL stringByReplacingOccurrencesOfString:@"asset:/" withString:@""];
    NSString *bundlePath = [[NSBundle mainBundle] pathForResource:[assetName stringByDeletingPathExtension]
                                                           ofType:[assetName pathExtension]];
    if (!bundlePath) return nil;

    NSString *ext = [assetName pathExtension];
    if (ext.length == 0) ext = @"bin";

    NSString *tempName = [NSString stringWithFormat:@"asset_%f.%@", [[NSDate date] timeIntervalSince1970], ext];
    NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempName];

    // overwrite if exists
    [[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];

    NSError *copyError = nil;
    [[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:tempPath error:&copyError];
    if (copyError) return nil;

    return [NSURL fileURLWithPath:tempPath];
  }

  // Otherwise: assume direct local path
  return [NSURL fileURLWithPath:pathOrURL];
}

- (void)ensureSTT
{
  if (!self.stt) {
    self.stt = [STT new];
    self.stt.delegate = self;
  }
}

- (void)wireTTSFinishedCallback
{
  if (!self.tts) return;

  __weak typeof(self) weakSelf = self;
  self.tts.onLastUtteranceFinished = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf || !strongSelf.hasListeners) return;

    // Match the old, working behavior
    dispatch_async(dispatch_get_main_queue(), ^{
      [strongSelf sendEventWithName:@"onFinishedSpeaking" body:@{}];
    });
  };
}

#pragma mark - Unified API

/// initAll({ locale: "en-US", model: "/path/model.onnx", timeoutMs?: 8000 })
RCT_EXPORT_METHOD(initAll:(NSDictionary *)opts
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_main_queue(), ^{
    if (self.initializing) { resolve(@"already_initializing"); return; }
    if (self.initialized)  { resolve(@"already_initialized");  return; }

    self.initializing = YES;

    NSString *locale = opts[@"locale"] ?: @"en-US";
    NSString *modelPath = opts[@"model"];
    if (modelPath.length == 0) {
      self.initializing = NO;
      reject(@"invalid_args", @"Missing 'model' in initAll()", nil);
      return;
    }

    // 1) STT first
    if (!self.stt) {
      self.stt = [STT new];
      self.stt.delegate = self;
    }
    [self.stt startSpeechWithLocaleStr:locale];

    // 2) TTS next
    NSURL *modelURL = nil;

    // ✅ BACKWARD COMPAT: plain "letters.onnx" (no slashes, no scheme) -> DO EXACTLY THIS.
    // Do NOT resolve, do NOT check existence here. Native core will search.
    BOOL isBareOnnxName =
      (modelPath != nil) &&
      ([modelPath rangeOfString:@"/"].location == NSNotFound) &&
      ([modelPath rangeOfString:@"://"].location == NSNotFound) &&
      ![modelPath hasPrefix:@"file://"] &&
      [[modelPath lowercaseString] hasSuffix:@".onnx"];

    if (isBareOnnxName) {
      modelURL = [NSURL fileURLWithPath:modelPath];
    } else {
      modelURL = [self resolveLocalURLFromPathOrURL:modelPath];
      if (!modelURL) {
        self.initializing = NO;
        [self.stt destroySpeech:nil];
        self.stt = nil;
        reject(@"bad_model", [NSString stringWithFormat:@"Could not resolve model path: %@", modelPath], nil);
        return;
      }

      // Verify file exists for local file URLs
      if (modelURL.isFileURL && ![[NSFileManager defaultManager] fileExistsAtPath:modelURL.path]) {
        self.initializing = NO;
        [self.stt destroySpeech:nil];
        self.stt = nil;
        reject(@"model_missing", [NSString stringWithFormat:@"Model file missing: %@", modelURL.path], nil);
        return;
      }
    }
    NSLog(@"[TTS] INIT: modelURL ==  %@", modelURL);

    NSError *err = nil;
    self.tts = [[DaVoiceTTS alloc] initWithModel:modelURL error:&err];
    if (err || !self.tts) {
      self.initializing = NO;
      [self.stt destroySpeech:nil];
      self.stt = nil;
      reject(@"tts_init_failed", err.localizedDescription ?: @"TTS init failed", err);
      return;
    }

    [self wireTTSFinishedCallback];

    self.initialized  = YES;
    self.initializing = NO;
    resolve(@"initialized");
  });
}

// Promise-based pause that resolves ONLY when iOS is actually settled in playback (mic released)
RCT_EXPORT_METHOD(pauseMicrophoneAsync:(nonnull NSNumber *)timeoutMs
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  if (!self.stt) { resolve(@{@"ok": @(NO), @"reason": @"no_stt"}); return; }

  // Default if caller passes null/0
  NSNumber *t = timeoutMs ?: @(1500);
  if (t.doubleValue <= 0) t = @(1500);

  // STT.swift will do the main-queue polling internally
  [self.stt pauseMicrophoneAndWait:t completion:^(BOOL ok, NSString * _Nullable reason) {
    resolve(@{@"ok": @(ok), @"reason": reason ?: @""});
  }];
}

// Promise-based unpause that resolves ONLY when engine+task are live again
RCT_EXPORT_METHOD(unPauseMicrophoneAsync:(nonnull NSNumber *)timeoutMs
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  if (!self.stt) { resolve(@{@"ok": @(NO), @"reason": @"no_stt"}); return; }

  NSNumber *t = timeoutMs ?: @(2500);
  if (t.doubleValue <= 0) t = @(2500);

  [self.stt unPauseMicrophoneAndWait:t completion:^(BOOL ok, NSString * _Nullable reason) {
    resolve(@{@"ok": @(ok), @"reason": reason ?: @""});
  }];
}

// ADD — pause mic
RCT_EXPORT_METHOD(pauseMicrophone:(RCTResponseSenderBlock)callback)
{
  if (!self.stt) { if (callback) callback(@[@(NO)]); return; }
  [self.stt pauseMicrophone];   // ← no colon
  if (callback) callback(@[@(YES)]);
}


// ADD — unpause mic
RCT_EXPORT_METHOD(unPauseMicrophone:(RCTResponseSenderBlock)callback)
{
  if (!self.stt) { if (callback) callback(@[@(NO)]); return; }
  [self.stt unPauseMicrophone]; // ← no colon
  if (callback) callback(@[@(YES)]);
}


RCT_EXPORT_METHOD(destroyAll:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  dispatch_async(dispatch_get_main_queue(), ^{
    if (!self.initialized && !self.initializing) {
      resolve(@"already_destroyed");
      return;
    }
    // prevent re-entry during destroy
    self.initializing = YES;

    // Destroy in reverse order: TTS -> STT
    @try { [self.tts stopSpeaking]; [self.tts destroy]; } @catch (__unused id e) {}
    self.tts = nil;

    @try { [self.stt destroySpeech:nil]; } @catch (__unused id e) {}
    self.stt = nil;

    self.initialized  = NO;
    self.initializing = NO;
    resolve(@"destroyed");
  });
}

#pragma mark - Convenience passthroughs (optional)

RCT_EXPORT_METHOD(startSpeech:(NSString *)locale
                  callback:(RCTResponseSenderBlock)callback)
{
  [self ensureSTT];
  [self.stt startSpeechWithLocaleStr:locale];
  if (callback) callback(@[@(NO)]);
}

RCT_EXPORT_METHOD(stopSpeech:(RCTResponseSenderBlock)callback)
{
  if (!self.stt) { if (callback) callback(@[@(NO)]); return; }
  [self.stt stopSpeech:^(BOOL ok) { if (callback) callback(@[@(NO)]); }];
}

RCT_EXPORT_METHOD(cancelSpeech:(RCTResponseSenderBlock)callback)
{
  if (!self.stt) { if (callback) callback(@[@(NO)]); return; }
  [self.stt cancelSpeech:^(BOOL ok) { if (callback) callback(@[@(NO)]); }];
}

RCT_EXPORT_METHOD(isSpeechAvailable:(RCTResponseSenderBlock)callback)
{
  [self ensureSTT];
  [self.stt isSpeechAvailable:^(BOOL ok){
    if (callback) callback(@[@(ok ? 1 : 0), [NSNull null]]);
  }];
}

RCT_EXPORT_METHOD(isRecognizing:(RCTResponseSenderBlock)callback)
{
  BOOL running = self.stt ? [self.stt isRecognizing] : NO;
  if (callback) callback(@[@(running ? 1 : 0)]);
}

RCT_EXPORT_METHOD(speak:(NSString *)text
                  speakerId:(nonnull NSNumber *)speakerId
                  speed:(nonnull NSNumber *)speed
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  if (!self.tts) { reject(@"no_tts", @"Call initAll first", nil); return; }

  float s = speed ? speed.floatValue : 1.0f;
  if (!isfinite(s) || s <= 0.0f) s = 1.0f;

  // NOTE: core native not changed yet; we'll actually apply speed in the Swift core next.
  // Always forward 3 args (text, sid, speed)
  [self.tts speak:text sid:speakerId.intValue speed:s];
  resolve(@"Speaking");
}

RCT_EXPORT_METHOD(stopSpeaking:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  if (!self.tts) { reject(@"no_tts", @"Call initAll first", nil); return; }
  [self.tts stopSpeaking];
  resolve(@"Stopped");
}

/// playWav(pathOrURL: string, markAsLast?: boolean)
RCT_EXPORT_METHOD(playWav:(NSString *)pathOrURL
                  markAsLast:(nonnull NSNumber *)markAsLast
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  if (!self.tts) {
    reject(@"no_tts", @"Call initAll first", nil);
    return;
  }

  if (pathOrURL == nil || pathOrURL.length == 0) {
    reject(@"bad_path", @"Empty pathOrURL", nil);
    return;
  }

  NSURL *fileURL = nil;

  // 1️⃣ Handle http(s) URLs — download to temporary file first
  if ([pathOrURL hasPrefix:@"http://"] || [pathOrURL hasPrefix:@"https://"]) {
    NSLog(@"[TTS] Downloading asset from URL: %@", pathOrURL);
    NSURL *remoteURL = [NSURL URLWithString:pathOrURL];
    if (!remoteURL) {
      reject(@"bad_url", @"Invalid remote URL", nil);
      return;
    }

    NSData *data = [NSData dataWithContentsOfURL:remoteURL];
    if (!data) {
      reject(@"download_failed", @"Failed to download remote asset", nil);
      return;
    }

    NSString *tempName = [NSString stringWithFormat:@"rn_asset_%f.wav", [[NSDate date] timeIntervalSince1970]];
    NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempName];
    if (![data writeToFile:tempPath atomically:YES]) {
      reject(@"write_failed", @"Failed to write temporary file", nil);
      return;
    }
    fileURL = [NSURL fileURLWithPath:tempPath];
    NSLog(@"[TTS] Downloaded to temp file: %@", tempPath);
  }

  // 2️⃣ Handle bundled asset:/ paths (copied from main bundle)
  else if ([pathOrURL hasPrefix:@"asset:/"]) {
    NSString *assetName = [pathOrURL stringByReplacingOccurrencesOfString:@"asset:/" withString:@""];
    NSLog(@"[TTS] Detected bundled asset: %@", assetName);
    NSString *bundlePath = [[NSBundle mainBundle] pathForResource:[assetName stringByDeletingPathExtension]
                                                           ofType:[assetName pathExtension]];
    if (!bundlePath) {
      reject(@"asset_missing", [NSString stringWithFormat:@"Asset not found in bundle: %@", assetName], nil);
      return;
    }
    // Copy to temp file so we have a writable/accessible URL
    NSString *tempName = [NSString stringWithFormat:@"asset_%f.wav", [[NSDate date] timeIntervalSince1970]];
    NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempName];
    NSError *copyError = nil;
    [[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:tempPath error:&copyError];
    if (copyError) {
      reject(@"asset_copy_failed", copyError.localizedDescription, copyError);
      return;
    }
    fileURL = [NSURL fileURLWithPath:tempPath];
    NSLog(@"[TTS] Copied bundled asset to temp: %@", tempPath);
  }

  // 3️⃣ Handle file:// URLs
  else if ([pathOrURL hasPrefix:@"file://"]) {
    fileURL = [NSURL URLWithString:pathOrURL];
  }

  // 4️⃣ Otherwise assume direct local path
  else {
    fileURL = [NSURL fileURLWithPath:pathOrURL];
  }

  // 5️⃣ Verify existence
  if (!fileURL || ![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]) {
    reject(@"file_missing", [NSString stringWithFormat:@"File missing: %@", fileURL.path], nil);
    return;
  }

  // 6️⃣ Play through TTS engine (queued)
  NSLog(@"[TTS] Playing file via DaVoiceTTS: %@", fileURL.path);
  [self.tts playWav:fileURL markAsLastUtterance:markAsLast.boolValue];
  resolve(@"queued");
}

/// playBuffer(desc: { base64, sampleRate, channels?, interleaved?, format: "i16" | "f32", markAsLast? })
RCT_EXPORT_METHOD(playBuffer:(NSDictionary *)desc
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  if (!self.tts) { reject(@"no_tts", @"Call initAll first", nil); return; }

  // Validate inputs
  NSString *b64 = [RCTConvert NSString:desc[@"base64"]];
  NSNumber *srN = [RCTConvert NSNumber:desc[@"sampleRate"]];
  NSString *fmt = [RCTConvert NSString:desc[@"format"]];
  NSNumber *chN = desc[@"channels"] ? [RCTConvert NSNumber:desc[@"channels"]] : @(1);
  NSNumber *ilN = desc[@"interleaved"] ? [RCTConvert NSNumber:desc[@"interleaved"]] : @(YES);
  NSNumber *markLastN = desc[@"markAsLast"] ? [RCTConvert NSNumber:desc[@"markAsLast"]] : @(YES);

  if (!b64 || !srN || !fmt) {
    reject(@"invalid_args", @"Missing one of base64/sampleRate/format", nil);
    return;
  }

  NSData *raw = SB_Base64Decode(b64);
  if (!raw) { reject(@"bad_base64", @"Could not decode base64 payload", nil); return; }

  AVAudioPCMBuffer *buf = SB_MakeMonoF32Buffer(raw, srN.doubleValue, fmt, chN.integerValue, ilN.boolValue);
  if (!buf) { reject(@"bad_buffer", @"Unsupported PCM layout or empty data", nil); return; }

  // Hand over to DaVoiceTTS (it will resample/normalize and enqueue via AEC graph)
  [self.tts playBuffer:buf markAsLastUtterance:markLastN.boolValue];
  resolve(@"queued");
}

@end
