import threading
import os
import time
import pjsua2 as pj
import uuid
import numpy as np
import webrtcvad
import queue

from bot.param import Constants
from bot.loggers import logger

from bot.k_sip_socket import k_STTClient

class MyAudioMediaPort(pj.AudioMediaPort):
    def __init__(self, call, company, on_hangup_requested):
        super().__init__()
        pj.AudioMediaPort.__init__(self)
  
        self.call_id = str(uuid.uuid4())
        self.on_hangup_requested = on_hangup_requested
        self.call_active = 0
        self.company = company
        
        self.audio_buffer = bytearray()
        self.vad = webrtcvad.Vad(Constants.VAD)
        self.is_vad = 0
        self.is_vad_start = 0
        
        self.sample_rate = Constants.SIP_SAMPLERATE
        self.one_second = Constants.SIP_ONE_SECOND_FRAME
        self.frame_size = Constants.SIP_FRAME_SIZE
        
        self.tts_audio = np.array([], dtype=np.int16)
        self.tts_pos = 0
        self.send = 0
        self.is_first = 0
        self.len_buffer = 0
        self.client_text = ""
        self.thread_lock = threading.Lock()
        
        self.robot_next = 0
        self.robot_counter = 0
        
        # 1 - Алло алло
        self.stt = k_STTClient(callback=self.stt_resv)
        
    def stt_resv(self, command_id, data):
        logger.warning(f" Получили новое аудио robot {command_id}")
        with self.thread_lock:
          
          if command_id == 1:
            if self.is_first == 1:
              self.is_first = 2
              
            self.tts_audio = np.concatenate([self.tts_audio, data])
            
          if command_id == 8:
            if data:
              self.client_text = data
            else:
              self.client_text = ".."

    def onFrameRequested(self, frame):
        #logger.debug(f"onFrameRequested {id(self)} {self.call_active} {self.send} {len(self.tts_audio)}")

        if self.call_active == 0:
            return
            
        if self.robot_next >= 3:
          return

        if self.is_first >= 2 and len(self.tts_audio)>0:
            
          frame.type = pj.PJMEDIA_FRAME_TYPE_AUDIO
          
          available = max(0, len(self.tts_audio) - self.tts_pos)
          if available <= 0:
            
              with self.thread_lock:
                self.robot_next += 1
                if self.robot_next == 2:
                  self.is_first = 4

                self.tts_audio = np.array([], dtype=np.int16)
                self.tts_pos = 0
                
              if self.robot_next == 1:
                self.stt.send_text(2, self.company)
 
              self.send = 2
              frame.buf[:] = bytes(frame.size)
            
              logger.warning(f"2 РОБОТ - закончил говорить {self.robot_next} {self.company}")
              return
            
          # 160 samples * 2 bytes = 320 bytes
          samples_needed = self.frame_size // 2  
        
          take_samples = min(samples_needed, available)
          chunk = self.tts_audio[self.tts_pos:self.tts_pos + take_samples]
          self.tts_pos += take_samples
          
          if take_samples < samples_needed:
              silence = np.zeros(samples_needed - take_samples, dtype=np.int16)
              chunk = np.concatenate([chunk, silence])

          frame.buf[:] = chunk.tobytes()

    """
    def is_silence(audio_chunk, silence_threshold=500):
      max_amplitude = max(abs(sample) for sample in audio_chunk)
      return max_amplitude < silence_threshold  # Порог тишины
    """
    
    def onFrameReceived(self, frame):
        #logger.debug(f"onFrameReceived {id(self)} {self.call_active} {self.send} {frame.size}")
        if frame.size != self.frame_size:
            if self.send >= 0:
              logger.warning(f"FRAME Size error {frame.size} {self.frame_size}")
              self.on_hangup_requested()
              self.send = -1

        if self.send == -1:
            self.diemedia()
          
        if self.call_active == 0:
            return
            
        ##
        try:
          is_speech = self.vad.is_speech(bytes(frame.buf), self.sample_rate)
          if is_speech:
              self.audio_buffer.extend(frame.buf)
              self.len_buffer = len(self.audio_buffer)
              self.is_vad = 0
          else:
              if self.is_vad <= 2:
                self.audio_buffer.extend(bytes(frame.size))
                
              self.is_vad += 1

        except Exception as e:
          if self.send != -1:
            logger.warning(f"Exception occurred: {str(e)} {e}\n{traceback.format_exc()}")
          self.send = -1

        #self.sample_rate * 0.02 * 2)  # размер буфера за 20 мс -> 50 Семплов * 
        #logger.warning(f"{self.is_first} - {frame.size} - {self.len_buffer} {self.is_vad_start} {self.robot_next}")
          
        if self.is_first >= 3 and self.robot_next>=2:
          if (
              (self.is_vad >= 3 and self.len_buffer >= self.one_second * Constants.STT_NEED_SECOND)
              or 
              (self.len_buffer >= self.one_second * Constants.STT_NEED_SECOND2 and self.is_vad >=1)
          ):
            logger.warning(f"3 ЧЕЛОВЕК - начал говорить")
            
            self.stt.send_audio(3, self.audio_buffer)
  
            self.audio_buffer = bytearray()
            self.is_vad = 0
            self.len_buffer = 0
 
        # Звонок активный - бот может сказать 1 сообдение
        if self.is_first == 0 and self.is_vad_start >= 5:
            logger.warning(f"1 РОБОТ - начал говорить")
            self.is_first = 1
            self.stt.send_audio(1, b".") 
            
        if self.robot_next == 2 and self.is_first == 4:
          self.robot_counter += 1

          if self.robot_counter == 70:
            # Але
            self.stt.send_audio(4, b".")
            
          if self.robot_counter == 200:
            #Завершаем диалог
            self.is_vad_start = 1001
         
        # Завершаем звонок по длительности
        if self.is_vad_start > 1000:
            self.on_hangup_requested()
            self.send = -1
            
        self.is_vad_start += 1

    def runvoice(self):
        logger.info(f"MEDIAPORT runvoice {id(self)}")
        self.audio_buffer = bytearray()
        self.send = 0
        self.call_active = 1
        
    def stopvoice(self):
        logger.warning(f"MEDIAPORT stopvoice {id(self)}")
        self.stt.get_text(8)
        self.send = -1

    def stopvoice2(self):
        logger.info(f"MEDIAPORT stopvoice2 {id(self)}")
        count = 0
        while self.client_text == "" and count < 8:
          count += 1
          time.sleep(0.25)
          
        if self.stt:
          logger.info(f"MEDIAPORT SOCKER {id(self)}")
          try:
            self.stt.closesocket()
          except Exception as e:
            pass
          self.audio_buffer = bytearray()
        self.stt = None
        
        return self.client_text
    
    def diemedia(self):
      logger.info(f"MEDIAPORT diemedia {id(self)}")
      self.send = -2
      self.call_active = 0
      self.is_first = -1

class Call(pj.Call):
    def __init__(self, acc, company, call_id=pj.PJSUA_INVALID_ID):
        super().__init__(acc, call_id)
        
        self.company = company

        self.med_port_status = 0
        
        self.end_call = 0
        self.aud_med = None
        self.med_port = None
        
        self.call_status = None
        self.call_code = None
        self.duration = 0
        self.call_record = ""
        self.clinent_text = ""
        self.filename1 = ""
        self.filename2 = ""
        
        self.recorder1 = None
        self.recorder2 = None
      
        self.completed = threading.Event()
   

    def onCallState(self, prm):
     
        ci = self.getInfo()
        
        logger.warning(f" Call-onCallState {ci.state} = disconnect {pj.PJSIP_INV_STATE_DISCONNECTED}  confirmed {pj.PJSIP_INV_STATE_CONFIRMED}")
        
        if ci.state == pj.PJSIP_INV_STATE_CONFIRMED:
            logger.warning(f"PJSIP_INV_STATE_CONFIRMED")
            
            # --- НАЧИНАЕМ ЗАПИСЬ ---
            self.filename1 = f"{Constants.SAVE_FILE}/1_{id(self)}.wav"
            self.recorder1 = pj.AudioMediaRecorder()
            self.recorder1.createRecorder(self.filename1)
            self.aud_med.startTransmit(self.recorder1)
            
            self.filename2 = f"{Constants.SAVE_FILE}/2_{id(self)}.wav"
            self.recorder2 = pj.AudioMediaRecorder()
            self.recorder2.createRecorder(self.filename2)
            self.med_port.startTransmit(self.recorder2)
            
            self.end_call = 1
            self.med_port.runvoice()

        elif ci.state == pj.PJSIP_INV_STATE_DISCONNECTED:
            logger.warning(f"PJSIP_INV_STATE_DISCONNECTED")
            
            # Если трубку подняли - значит
            if self.end_call == 1:
              self.end_call = 2

            if self.aud_med and self.recorder1:
              self.aud_med.stopTransmit(self.recorder1)
              self.med_port.stopTransmit(self.recorder2)
              self.recorder1 = None
              self.recorder2 = None

              self.aud_med.stopTransmit(self.med_port)
              self.med_port.stopTransmit(self.aud_med)
   
            if self.med_port_status == 1:
              self.med_port.stopvoice()
              self.clinent_text = self.med_port.stopvoice2()
           
            if not self.call_status:
                if ci.lastStatusCode == 200:
                    self.duration = ci.connectDuration.sec if ci.connectDuration.sec > 0 else 0
                    
                if self.duration == 0:
                    if self.filename1 and os.path.isfile(self.filename1):
                      os.remove(self.filename1)
                    if self.filename2 and os.path.isfile(self.filename2):
                      os.remove(self.filename2)

            self.call_code = ci.lastStatusCode
            self.call_record = id(self)

            self.completed.set()
            
            del self.med_port
            del self.aud_med

            self.aud_med = None
            self.med_port = None

            self.med_port_status = 0

            logger.warning(f" Звонок {id(self)} завершен")

    def on_hangup_requested(self):
        # logger.warning(f"Call-on_hangup_requested {id(self)}")
        try:
            call_info = self.getInfo()

            if call_info.state != pj.PJSIP_INV_STATE_DISCONNECTED:
                prm = pj.CallOpParam()
                prm.statusCode = 200
                self.hangup(prm)

        except Exception as e:
            logger.warning(f"Exception in on_hangup_requested: {e}")

    def onCallMediaState(self, prm):

        ci = self.getInfo()

        for media_info in ci.media:
          
            #logger.warning(f" Call-onCallMediaState media_info.status {media_info.status} act-{pj.PJSUA_CALL_MEDIA_ACTIVE}")
            
            if media_info.status == pj.PJSUA_CALL_MEDIA_ACTIVE and media_info.type == pj.PJMEDIA_TYPE_AUDIO:
              
                if self.med_port_status == 0:
                    self.med_port_status = 1
   
                    self.aud_med = self.getAudioMedia(-1)
                    self.med_port = MyAudioMediaPort(self, self.company, self.on_hangup_requested)
            
                    fmt = pj.MediaFormatAudio()
                    fmt.type = pj.PJMEDIA_TYPE_AUDIO
                    fmt.clockRate = Constants.SIP_SAMPLERATE
                    fmt.channelCount = 1
                    fmt.bitsPerSample = 16
                    fmt.frameTimeUsec = 20000
                    
                    self.med_port.createPort("med_port", fmt)
                    self.med_port.startTransmit(self.aud_med)
                    self.aud_med.startTransmit(self.med_port)
                    
                    # --- НАЧИНАЕМ ЗАПИСЬ ---
                    #self.filename1 = f"{Constants.SAVE_FILE}/1_{id(self)}.wav"
                    #self.recorder1 = pj.AudioMediaRecorder()
                    #self.recorder1.createRecorder(self.filename1)
                    #self.aud_med.startTransmit(self.recorder1)
                    
                    #self.filename2 = f"{Constants.SAVE_FILE}/2_{id(self)}.wav"
                    #self.recorder2 = pj.AudioMediaRecorder()
                    #self.recorder2.createRecorder(self.filename2)
                    #self.med_port.startTransmit(self.recorder2)

    def wait_for_completion(self, timeout=None):
        #logger.warning(f" Call-wait_for_completion {id(self)}")
        
        self.completed.wait(timeout)
        self.completed.clear()
        return {
            'code': self.call_code,
            'duration': self.duration,
            'text': self.clinent_text,
            'record': self.call_record
        }

class Account(pj.Account):
    def __init__(self):
        pj.Account.__init__(self)
        self.is_reg = False

    def onRegState(self, prm):
        self.is_reg = prm.code == 200
        logger.warning(f"Registration state: {'Registered' if self.is_reg else 'Unregistered'}")

    def onIncomingCall(self, iprm):
        c = call.Call(self, call_id=prm.callId)
        call_prm = pj.CallOpParam()
        call_prm.statusCode = 180
        c.hangup(call_prm)

class SIPWorker:
    def __init__(self, sip_server, sip_user, sip_pass):
      self.sip_server = sip_server
      self.sip_user = sip_user
      self.sip_pass = sip_pass
      #self.acc_cfg = acc_cfg
      self.command_queue = queue.Queue()
      self.result_queue = queue.Queue()

      self.running = False
      self.company = None

      self.call = None
      self.call_prm = None

      self.last_call_time = 0

    def start(self):
      if self.running:
          return
          
      self.running = True
      self.thread = threading.Thread(target=self._run, daemon=True)
      self.thread.start()
  
    def _run(self):
  
      self.ep = pj.Endpoint()
      self.ep.libCreate()
      
      # Конфигурация endpoint
      ep_cfg = pj.EpConfig()
      ep_cfg.medConfig.maxCalls = 3
      ep_cfg.medConfig.threadCnt = 0

      ep_cfg.medConfig.threadCnt = 0  # No worker threads for audio
      #ep_cfg.medConfig.noVad = True   # Disable VAD (Voice Activity Detection) if not needed
      #ep_cfg.medConfig.noAudioDev = True

      ep_cfg.logConfig.level = 0
      ep_cfg.logConfig.consoleLevel = 0
       
      self.ep.libInit(ep_cfg)
      # Регистрируем текущий поток
      #self.ep.libRegisterThread("SIPWorkerThread")

      tcfg = pj.TransportConfig()
      tcfg.port = 5060
      tcfg.qosType = pj.PJ_QOS_TYPE_VOICE
      tcfg.portRange = 0
      self.ep.transportCreate(pj.PJSIP_TRANSPORT_UDP, tcfg)

      acc_cfg = pj.AccountConfig()
      acc_cfg.idUri = f"sip:{self.sip_user}@{self.sip_server}"
      acc_cfg.regConfig.registrarUri = f"sip:{self.sip_server};expires=3600"
      
      cred = pj.AuthCredInfo("digest", "*", self.sip_user, 0, self.sip_pass)
      acc_cfg.sipConfig.authCreds.append(cred)

      # Устанавливаем интервал регистрации (в секундах)
      acc_cfg.regConfig.retryIntervalSec = 60
      acc_cfg.regConfig.firstRetryIntervalSec = 60
      acc_cfg.regConfig.delayBeforeRefreshSec = 120

      # Если сервер требует определенный Expires
      acc_cfg.regConfig.registrarTimeoutSec = 3600

      self.acc = Account()
      self.acc.create(acc_cfg)
      
      self.ep.instance().audDevManager().setNullDev()
     
      self.ep.libStart()

      # поддержка регистрации
      CHECK_INTERVAL = 60  # раз в минуту проверяем
      REREGISTER_TIMEOUT = 15*60  # 5 минут
      last_check = 0

      self.call_prm = pj.CallOpParam(True)
      self.call_prm.opt.audioCount = 1
      self.call_prm.opt.videoCount = 0
      self.call_prm.opt.timeoutSec = 5

      # Основной цикл обработки
      while self.running:
        # Обработка команд

        now = time.time()
        if now - last_check > CHECK_INTERVAL:
            last_check = now

            if self.acc.is_reg:

                idle = now - self.last_call_time

                if idle > REREGISTER_TIMEOUT:
                    self.last_call_time = now
                    logger.warning("15 минут нет звонков, перерегистрация")
                    self.acc.setRegistration(False)
                    time.sleep(2)
                    self.acc.setRegistration(True)

        try:
            cmd = self.command_queue.get_nowait()
        except queue.Empty:
            cmd = None  # Или другой способ обработки пустой очереди

        if cmd is not None:
            if cmd[0] == 'call':
              self._make_call(cmd[1])
            if cmd[0] == 'stop':
              break
        else:
            self.ep.libHandleEvents(10)

        time.sleep(0.5)
     
      self._cleanup()
            
    def reg_company(self, company):
      self.company = company
      
      

    def _make_call(self, phone):
      
        result = {'status': 'error', 'message': 'noreg'}
        
        if self.acc.is_reg == True:
          
          try:
              call = Call(acc=self.acc, company=self.company)

              logger.warning(f"Звонок на: {phone}")
               
              call.makeCall(f"sip:{phone}@{self.sip_server}", self.call_prm)
              
              self.last_call_time = time.time()
      
              result = call.wait_for_completion()

              logger.warning(f"Результат звонка: {result}")
              
              del call
              
              time.sleep(3)

          except Exception as e:
              result = {'status': 'error', 'message': str(e)}

        return self.result_queue.put(result)

    def make_call(self, phone):
      self.command_queue.put(('call', phone))
      return self.result_queue.get()

    def make_calloff(self, phone):
      self.command_queue.put(('call', phone))
      return self.result_queue.get()

    def _cleanup(self):
      if hasattr(self, 'ep') and self.ep:
        self.ep.libDestroy()

    def stop(self):
      self.command_queue.put(('stop', None))
      self.running = False
      if self.thread:
        self.thread.join()

class k_SIP:
    def __init__(self, sip_server, sip_user, sip_pass):
        self.worker = SIPWorker(sip_server, sip_user, sip_pass)

    def reg_company(self, company):
        return self.worker.reg_company(company)
        
    def make_call(self, phone):
        return self.worker.make_call(phone)

    def start(self):
        self.worker.start()
         
    def stop(self):
        self.worker.stop()


Что-то с завершением не то
2025-06-30 15:57:15,101 - vb - WARNING - Звонок на: 73452518426
2025-06-30 15:57:15,102 - vb - WARNING -  Call-onCallState 1 = disconnect 6  confirmed 5
2025-06-30 15:57:22,647 - vb - WARNING -  Call-onCallState 4 = disconnect 6  confirmed 5
2025-06-30 15:57:22,648 - vb - WARNING -  Call-onCallState 5 = disconnect 6  confirmed 5
2025-06-30 15:57:22,649 - vb - WARNING - PJSIP_INV_STATE_CONFIRMED
2025-06-30 15:57:22,649 - vb - INFO - MEDIAPORT runvoice 140119497236928
2025-06-30 15:57:22,700 - vb - WARNING - FRAME Size error 0 320
2025-06-30 15:57:22,701 - vb - WARNING -  Call-onCallState 6 = disconnect 6  confirmed 5
2025-06-30 15:57:22,701 - vb - WARNING - PJSIP_INV_STATE_DISCONNECTED
Traceback (most recent call last):
  File "/home/user/voice_bot/bot/k_sip.py", line 281, in onCallState
    self.aud_med.stopTransmit(self.recorder1)
  File "/root/.local/lib/python3.9/site-packages/pjsua2.py", line 5634, in stopTransmit
    return _pjsua2.AudioMedia_stopTransmit(self, sink)
pjsua2.Error
2025-06-30 15:57:22,702 - vb - INFO - MEDIAPORT diemedia 140119497236928