import importlib
import os
import signal
import subprocess
import threading
import time
import wave
from datetime import datetime

import numpy as np
import pandas as pd
import pyaudio  # 使用 PyAudio 来精确控制 ALSA 设备
from flask import Flask, render_template, jsonify
from flask_socketio import SocketIO, emit

# ---------------------------------------------------------
# 可配置：GPIOZERO_PIN_FACTORY 默认 mock，便于无硬件先启动
# 在真机上想用 lgpio / pigpio：运行前 export GPIOZERO_PIN_FACTORY=lgpio|pigpio
# ---------------------------------------------------------
def _detect_default_pin_factory() -> str:
    explicit = os.getenv("GPIOZERO_PIN_FACTORY")
    if explicit:
        return explicit

    candidates = [
        ("gpiozero.pins.lgpio", "lgpio"),
        ("gpiozero.pins.pigpio", "pigpio"),
        ("gpiozero.pins.rpigpio", "rpigpio"),
        ("gpiozero.pins.native", "native"),
    ]

    errors = []
    for module_name, factory_name in candidates:
        try:
            importlib.import_module(module_name)
            return factory_name
        except ImportError as exc:
            errors.append(f"{factory_name}: {exc}")

    tried = '; '.join(errors) or '无可用驱动模块'
    raise RuntimeError(f"未检测到可用的 gpiozero pin factory，请安装 python3-lgpio 或设置 GPIOZERO_PIN_FACTORY (已尝试: {tried})")


DEFAULT_PIN_FACTORY = _detect_default_pin_factory()

# --- 配置 ---
# ALSA 设备参数
ALSA_DEVICE_NAME = "hw:2,0"  # USB 声卡
CHANNELS = 1
RATE = 48000
FORMAT = pyaudio.paInt16  # S16_LE
FORMAT_WIDTH = pyaudio.get_sample_size(FORMAT)

# 录制参数
CHUNK_SIZE = 4800  # 每次读取 0.1 秒数据 (48000 / 10)
MAX_RECORD_SECONDS = 10 * 60  # 10 分钟

# 路径设置：基于当前模块目录，避免工作目录不同导致的相对路径错误
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SAVE_DIR = os.path.join(BASE_DIR, "file", "voice")
TEMPLATE_DIR = os.path.join(BASE_DIR, "templates")
STATIC_DIR = os.path.join(BASE_DIR, "static")

# --- 全局（由 create_app 初始化）---
socketio: SocketIO | None = None
listen_process = None  # 实时监听子进程
work_thread = None     # 录制与分析线程
stop_work_event = threading.Event()  # 通知工作线程停止

# =========================================================
# 工具函数
# =========================================================

def calculate_dominant_frequency(data_bytes: bytes, rate: int) -> float:
    """计算音频数据块的主频率"""
    try:
        audio_data = np.frombuffer(data_bytes, dtype=np.int16)
        if len(audio_data) == 0:
            return 0.0
        window = np.hamming(len(audio_data))
        audio_data = audio_data * window
        fft_data = np.abs(np.fft.rfft(audio_data))
        freqs = np.fft.rfftfreq(len(audio_data), 1.0 / rate)
        if len(freqs) > 1:
            peak_index = int(np.argmax(fft_data[1:]) + 1)
            return float(freqs[peak_index])
        return 0.0
    except Exception as e:
        print(f"FFT Error: {e}")
        return 0.0


def get_device_index(pyaudio_instance: pyaudio.PyAudio, device_name: str) -> int:
    """通过名称查找 PyAudio 设备索引"""
    for i in range(pyaudio_instance.get_device_count()):
        info = pyaudio_instance.get_device_info_by_index(i)
        if device_name in info.get('name', '') and info.get('maxInputChannels', 0) >= CHANNELS:
            print(f"找到设备: {info['name']} (Index: {i})")
            return i
    return -1


def work_thread_func(filename_base: str):
    """工作线程：录制、保存 WAV、计算 FFT、通过 SocketIO 发送数据"""
    global stop_work_event, work_thread

    wav_filename = os.path.join(SAVE_DIR, f"{filename_base}.wav")
    csv_filename = os.path.join(SAVE_DIR, f"{filename_base}.csv")

    p = pyaudio.PyAudio()
    stream = None
    wf = None

    frequency_data_list: list[dict] = []  # (工作时间, 频率)
    start_time = time.time()

    try:
        device_index = get_device_index(p, ALSA_DEVICE_NAME)
        if device_index == -1:
            msg = f"错误：找不到 ALSA 设备 '{ALSA_DEVICE_NAME}'"
            print(msg)
            if socketio:
                socketio.emit('status_update', {'msg': msg})
            return

        stream = p.open(
            format=FORMAT,
            channels=CHANNELS,
            rate=RATE,
            input=True,
            input_device_index=device_index,
            frames_per_buffer=CHUNK_SIZE,
        )

        os.makedirs(SAVE_DIR, exist_ok=True)
        wf = wave.open(wav_filename, 'wb')
        wf.setnchannels(CHANNELS)
        wf.setsampwidth(FORMAT_WIDTH)
        wf.setframerate(RATE)

        print("--- 开始工作：录制和分析 ---")
        if socketio:
            socketio.emit('status_update', {'msg': '工作开始：正在录制和分析...'})

        while not stop_work_event.is_set():
            elapsed_time = time.time() - start_time
            if elapsed_time >= MAX_RECORD_SECONDS:
                msg = '达到 10 分钟最大时长，自动停止录制。'
                print("--- " + msg + " ---")
                if socketio:
                    socketio.emit('status_update', {'msg': msg})
                break

            try:
                data = stream.read(CHUNK_SIZE, exception_on_overflow=False)
            except IOError as e:
                # PyAudio 的溢出错误码在不同版本略有差异，统一忽略一次并继续
                print(f"警告：输入缓冲区问题，跳过本次块：{e}")
                continue

            wf.writeframes(data)
            freq = calculate_dominant_frequency(data, RATE)
            frequency_data_list.append({'work_time_s': elapsed_time, 'frequency_hz': freq})

            if socketio:
                socketio.emit('new_frequency_data', {'time': elapsed_time, 'frequency': freq})

    except Exception as e:
        print(f"工作线程出错: {e}")
        if socketio:
            socketio.emit('status_update', {'msg': f'错误: {e}'})
    finally:
        print("--- 工作停止，开始清理 ---")
        try:
            if stream:
                stream.stop_stream()
                stream.close()
        finally:
            if p:
                p.terminate()
        if wf:
            wf.close()

        if frequency_data_list:
            try:
                df = pd.DataFrame(frequency_data_list)
                df.to_csv(csv_filename, index=False, float_format='%.2f')
                print(f"CSV 文件已保存到: {csv_filename}")
                if socketio:
                    socketio.emit('status_update', {'msg': '工作结束。WAV 和 CSV 已保存。'})
            except Exception as e:
                print(f"CSV 保存失败: {e}")
                if socketio:
                    socketio.emit('status_update', {'msg': f'CSV 保存失败: {e}'})
        else:
            if socketio:
                socketio.emit('status_update', {'msg': '工作结束。没有数据被记录。'})

        total_elapsed = time.time() - start_time
        if socketio:
            socketio.emit('work_finished', {'duration': round(total_elapsed, 2), 'success': bool(frequency_data_list)})

        stop_work_event.clear()
        work_thread = None
        print("--- 工作线程清理完毕 ---")


# =========================================================
# Flask 应用工厂 + 路由
# =========================================================

def create_app() -> Flask:
    """应用工厂：延迟导入蓝图，避免导入期触发硬件初始化。"""
    # 在导入 knock 之前，确保 pin factory 有个可用的默认值（mock/开发友好）
    os.environ.setdefault("GPIOZERO_PIN_FACTORY", DEFAULT_PIN_FACTORY)

    app = Flask(__name__, template_folder=TEMPLATE_DIR, static_folder=STATIC_DIR)
    app.config['SECRET_KEY'] = 'your_secret_key!'

    # 只有在创建 app 时才导入蓝图，降低导入期副作用
    from cam.app import video0_bp
    from driver.app import driver_bp
    from knock.app import knock_bp
    from slide.app import slider_bp

    # 蓝图挂载
    app.register_blueprint(video0_bp)  # camera 自行定义前缀
    app.register_blueprint(driver_bp, url_prefix='/driver')
    app.register_blueprint(knock_bp, url_prefix='/knock')
    app.register_blueprint(slider_bp, url_prefix='/slider')

    # 首页
    @app.route('/')
    def index():
        return render_template('dashboard.html')

    # 健康检查（可被前端轮询）
    @app.get('/health')
    def health():
        return jsonify({
            'ok': True,
            'pin_factory': os.getenv('GPIOZERO_PIN_FACTORY'),
            'alsa': ALSA_DEVICE_NAME,
        })

    # 实时监听控制
    @app.post('/start_listen')
    def start_listen():
        global listen_process, work_thread
        if listen_process and listen_process.poll() is None:
            return jsonify({'status': 'error', 'message': '监听已在运行'}), 400
        if work_thread and work_thread.is_alive():
            return jsonify({'status': 'error', 'message': '“工作”正在运行，请先停止工作再监听。'}), 400
        try:
            cmd = f"arecord -D {ALSA_DEVICE_NAME} -c {CHANNELS} -r {RATE} -f S16_LE | aplay -"
            listen_process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setsid)
            print("--- 实时监听已启动 ---")
            if socketio:
                socketio.emit('status_update', {'msg': '实时监听已启动'})
            return jsonify({'status': 'success', 'message': '实时监听已启动'})
        except Exception as e:
            return jsonify({'status': 'error', 'message': str(e)}), 500

    @app.post('/stop_listen')
    def stop_listen():
        global listen_process
        if listen_process and listen_process.poll() is None:
            try:
                os.killpg(os.getpgid(listen_process.pid), signal.SIGTERM)
                listen_process.wait()
                listen_process = None
                print("--- 实时监听已停止 ---")
                if socketio:
                    socketio.emit('status_update', {'msg': '实时监听已停止'})
                return jsonify({'status': 'success', 'message': '实时监听已停止'})
            except Exception as e:
                return jsonify({'status': 'error', 'message': str(e)}), 500
        else:
            return jsonify({'status': 'error', 'message': '监听未在运行'}), 400

    # 工作：录制与分析
    @app.post('/start_work')
    def start_work():
        global work_thread, stop_work_event, listen_process
        if listen_process and listen_process.poll() is None:
            return jsonify({'status': 'error', 'message': '“实时监听”正在运行，请先停止监听再工作。'}), 400
        if work_thread and work_thread.is_alive():
            return jsonify({'status': 'error', 'message': '工作线程已在运行'}), 400
        stop_work_event.clear()
        filename_base = datetime.now().strftime("rec_%Y%m%d_%H%M%S")
        t = threading.Thread(target=work_thread_func, args=(filename_base,), daemon=True)
        work_thread = t
        t.start()
        return jsonify({'status': 'success', 'message': '工作线程已启动'})

    @app.post('/stop_work')
    def stop_work():
        global work_thread, stop_work_event
        if work_thread and work_thread.is_alive():
            stop_work_event.set()
            print("--- 发送停止信号到工作线程 ---")
            if socketio:
                socketio.emit('status_update', {'msg': '正在停止工作线程...'})
            return jsonify({'status': 'success', 'message': '正在停止工作线程...'})
        else:
            return jsonify({'status': 'error', 'message': '工作线程未在运行'}), 400

    return app


# =========================================================
# SocketIO 事件（在 create_socketio 里绑定）
# =========================================================

def create_socketio(app: Flask) -> SocketIO:
    global socketio
    socketio = SocketIO(app, async_mode='threading', cors_allowed_origins='*')

    @socketio.on('connect')
    def handle_connect(auth=None):
        print(f'--- 客户端已连接 (Auth: {auth}) ---')
        emit('status_update', {'msg': '已连接到服务器'})

    @socketio.on('disconnect')
    def handle_disconnect():
        print('--- 客户端已断开 ---')

    return socketio


# =========================================================
# 入口
# =========================================================
if __name__ == '__main__':
    # 运行：在项目根目录执行 `python -m voice.app` 会更稳妥
    # 但直接 `python voice/app.py` 也可（取决于你的包结构）
    print("--- 启动 Flask-SocketIO 服务器 ---")
    app = create_app()
    socketio = create_socketio(app)
    socketio.run(app, host='0.0.0.0', port=5000, debug=False, allow_unsafe_werkzeug=True)
