import os
import tempfile
import shutil
import time
import json
from datetime import datetime, timedelta
from flask import Blueprint, render_template, request, jsonify, g, redirect, url_for, current_app, flash, session, Response, stream_with_context
from app.services.deployment import docker_service
from app.services.auth.decorators import login_required
from app.models.database.operations import (
    get_challenge_record, create_deployment_record, get_deployment_by_uuid,
    get_deployments_by_user, update_deployment_status, get_deployment_logs,
    delete_deployment_record
)

bp = Blueprint('deployment', __name__)

# 简易解析 docker-compose 端口映射，返回 [(host, external, internal)]
def _extract_port_mappings(working_dir: str):
    try:
        import os, re
        # 1) 先在根目录查找，找不到则递归查找
        compose_path = None
        candidates = ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml")
        for name in candidates:
            p = os.path.join(working_dir, name)
            if os.path.exists(p):
                compose_path = p
                break
        if not compose_path:
            for root, _dirs, files in os.walk(working_dir):
                for f in files:
                    if f in candidates:
                        compose_path = os.path.join(root, f)
                        break
                if compose_path:
                    break
        if not compose_path:
            return []
        with open(compose_path, 'r', encoding='utf-8', errors='ignore') as rf:
            content = rf.read()
        # 匹配诸如 - "0.0.0.0:8080:80"、- "8080:80"、- 8080:80
        mappings = re.findall(r"-\s*[\"']?(?:(\d+\.\d+\.\d+\.\d+):)?(\d+):(\d+)[\"']?", content)
        result = []
        for host, ext, inter in mappings:
            result.append((host or "0.0.0.0", int(ext), int(inter)))
        # 去重并保持顺序
        seen = set()
        uniq = []
        for m in result:
            key = (m[0], m[1], m[2])
            if key not in seen:
                seen.add(key)
                uniq.append(m)
        return uniq
    except Exception:
        return []


@bp.route('/select-challenge', methods=['GET'])
@bp.route('/deploy', methods=['GET'])
@login_required
def select_challenge():
    """选择题目进行部署 - 重定向到主页面"""
    return redirect(url_for('deployment.index'))


@bp.route('/', methods=['GET'])
@login_required
def index():
    """部署管理主页"""
    if not hasattr(g, 'user') or not g.user:
        return redirect(url_for('auth.login'))

    # 获取当前用户的部署记录
    deployments = get_deployments_by_user(g.user.id)

    # 为每条记录动态补充名称与多端口信息（不改动数据库结构）
    # 优先从X-Forwarded-Host获取(代理/负载均衡场景),否则使用request.host或环境变量
    host = request.headers.get('X-Forwarded-Host') or request.host.split(':')[0] or os.environ.get('HOST_ADDRESS', 'localhost')
    for d in deployments:
        try:
            # 名称补充：优先题目名，其次容器名，最后用短ID占位
            display_name = None
            if d.get('challenge_id'):
                try:
                    challenge = get_challenge_record(int(d.get('challenge_id')))
                except Exception:
                    challenge = None
                if challenge:
                    d['challenge'] = challenge
                    display_name = (challenge.get('name') or '').strip()
            if not display_name:
                display_name = (d.get('container_name') or '').strip()
            if not display_name:
                short_id = (d.get('deployment_uuid') or d.get('id') or '')[:8]
                display_name = f"部署#{short_id}" if short_id else "未命名题目"
            d['display_name'] = display_name

            # 获取实时容器状态
            try:
                working_directory = d.get('working_directory')
                if working_directory:
                    status_result = docker_service.get_container_status(d.get('deployment_uuid'), working_directory)
                    if status_result.get('status') == 'success':
                        container_status = status_result.get('container_status')
                        if container_status and container_status != d.get('status'):
                            d['status'] = container_status
                            # 更新数据库状态
                            from app.models.database.operations import update_deployment_status
                            update_deployment_status(d.get('deployment_uuid'), container_status)
            except Exception as e:
                print(f"获取部署状态失败: {str(e)}")
                import traceback
                traceback.print_exc()

            # 端口与访问地址补充 - 优先使用数据库中保存的 external_port
            if d.get('external_port'):
                # 使用数据库中保存的实际端口
                d['access_urls'] = [f"http://{host}:{d.get('external_port')}"]
            else:
                # 回退：从 docker-compose.yml 解析端口
                wd = d.get('working_directory')
                ports = _extract_port_mappings(wd) if wd else []
                if ports:
                    # 去重端口映射，避免重复显示
                    unique_ports = []
                    seen_mappings = set()
                    for h, ext, inter in ports:
                        mapping_key = (h, ext, inter)
                        if mapping_key not in seen_mappings:
                            seen_mappings.add(mapping_key)
                            unique_ports.append((h, ext, inter))

                    d['port_mappings'] = unique_ports
                    # 生成访问地址列表（仅对常见http端口给出http链接）
                    urls = []
                    for h, ext, inter in unique_ports:
                        if inter in (80, 8080, 8000, 5000, 443, 3000):
                            scheme = 'https' if inter == 443 else 'http'
                            urls.append(f"{scheme}://{host}:{ext}")
                    d['access_urls'] = urls
        except Exception as e:
            print(f"处理部署记录时出错: {str(e)}")
            pass

    # 获取用户的题目列表（用于新建部署）
    from app.models.database.operations import get_challenges_by_user
    challenges = get_challenges_by_user(g.user.id)

    # 为每个题目添加部署统计
    deployment_map = {}
    for d in deployments:
        if d.get('challenge_id'):
            cid = int(d['challenge_id'])
            if cid not in deployment_map:
                deployment_map[cid] = []
            deployment_map[cid].append(d)

    for challenge in challenges:
        challenge_deployments = deployment_map.get(challenge['id'], [])
        running_count = sum(1 for d in challenge_deployments if d.get('status') == 'running')
        challenge['deployment_stats'] = {
            'total': len(challenge_deployments),
            'running': running_count,
            'has_deployment': len(challenge_deployments) > 0
        }

    return render_template(
        'pages/deployment/index.html',
        deployments=deployments,
        challenges=challenges
    )

@bp.route('/execute/<int:challenge_id>', methods=['POST', 'GET'])
@login_required
def execute_deployment(challenge_id):
    """部署指定的题目

    Args:
        challenge_id: 题目ID
    """
    if not hasattr(g, 'user') or not g.user:
        flash('请先登录', 'danger')
        return redirect(url_for('auth.login'))

    # GET请求重定向到部署页面
    if request.method == 'GET':
        return redirect(url_for('deployment.select_challenge'))

    # 检查是否请求流式输出
    stream_output = request.headers.get('Accept') == 'text/event-stream'

    # POST请求处理部署操作
    # 获取题目记录
    challenge = get_challenge_record(challenge_id)
    if not challenge:
        if stream_output:
            def generate():
                yield "获取题目失败: 题目不存在 (ID: {})".format(challenge_id)
            return Response(stream_with_context(generate()), content_type='text/event-stream')
        else:
            return jsonify({
                'status': 'error',
                'message': f'题目不存在: {challenge_id}'
            })

    # 确保用户有权限访问此题目
    if challenge.get('user_id') != g.user.id and g.user.role != 'admin':
        if stream_output:
            def generate():
                yield "权限错误: 您没有权限部署此题目"
            return Response(stream_with_context(generate()), content_type='text/event-stream')
        else:
            return jsonify({
                'status': 'error',
                'message': '您没有权限部署此题目'
            })

    # 获取部署选项
    # 端口范围：42500-42600
    PORT_MIN = 42500
    PORT_MAX = 42600
    port = request.form.get('port', None)
    if port:
        try:
            port = int(port)
            # 验证端口范围
            if port < PORT_MIN or port > PORT_MAX:
                port = None  # 超出范围则使用自动分配
        except ValueError:
            port = None

    timeout_days = request.form.get('timeout_days', 1)
    try:
        timeout_days = int(timeout_days)
        if timeout_days < 1:
            timeout_days = 1
        elif timeout_days > 7:  # 最多7天
            timeout_days = 7
    except ValueError:
        timeout_days = 1

    # 获取用户访问的IP地址
    # 优先从X-Forwarded-For获取(代理/负载均衡场景),否则使用request.host
    host_address = request.headers.get('X-Forwarded-Host') or request.host.split(':')[0]

    # 部署选项
    deployment_options = {
        'port': port,
        'timeout': timeout_days * 86400,  # 转换为秒
        'timeout_days': timeout_days,  # 保留天数，用于创建部署记录
        'stream_output': stream_output,
        'user_id': g.user.id,  # 添加用户ID，确保在流式部署时可以获取到
        'host_address': host_address  # 添加用户访问的IP地址
    }

    # 如果是流式输出
    if stream_output:
        def generate():
            yield "开始部署题目: {}".format(challenge.get('name', '未命名题目'))
            yield "正在准备部署环境..."

            try:
                # 获取题目的输出目录
                output_dir = challenge.get('output_dir')
                
                if not output_dir:
                    yield "错误: 题目没有输出目录信息"
                    return
                
                # 检查输出目录是否存在
                if not os.path.exists(output_dir):
                    yield f"错误: 输出目录不存在: {output_dir}"
                    return
                
                # 查找 docker 子目录
                docker_dir = os.path.join(output_dir, 'docker')
                if not os.path.exists(docker_dir):
                    yield f"错误: Docker目录不存在: {docker_dir}"
                    return
                
                yield f"找到Docker目录: {docker_dir}\n"
                yield "开始执行部署...\n"

                # 直接使用 docker 目录进行部署
                for output in docker_service.execute_deployment_stream(challenge_id, docker_dir, deployment_options):
                    # 确保每行输出都有换行符
                    if not output.endswith('\n'):
                        yield output + '\n'
                    else:
                        yield output

                yield "部署过程完成\n"

            except Exception as e:
                yield "部署过程中出错: {}".format(str(e))
                import traceback
                traceback.print_exc()


        return Response(stream_with_context(generate()), content_type='text/event-stream')

    # 非流式输出 - 原始处理方式
    # 创建临时目录
    with tempfile.TemporaryDirectory() as temp_dir:
        # 提取题目代码
        try:
            # 从数据库获取题目文件内容
            code_files = challenge.get('code_files', {})
            if not code_files:
                return jsonify({
                    'status': 'error',
                    'message': '题目没有代码文件'
                })

            # 创建目录结构并写入文件
            for file_path, file_content in code_files.items():
                # 跳过空文件和目录
                if not file_content:
                    continue

                # 创建目录
                full_path = os.path.join(temp_dir, file_path)
                os.makedirs(os.path.dirname(full_path), exist_ok=True)

                # 写入文件内容
                with open(full_path, 'w', encoding='utf-8') as f:
                    f.write(file_content)

            # 执行部署
            result = docker_service.execute_deployment(challenge_id, temp_dir, deployment_options)

            if result.get('status') == 'success':
                # 部署成功，创建数据库记录
                deployment_data = result.get('deployment', {})
                deployment_data['user_id'] = g.user.id

                # 添加到数据库
                record = create_deployment_record(deployment_data)

                return jsonify({
                    'status': 'success',
                    'message': result.get('message', '部署成功'),
                    'deployment_id': deployment_data.get('id'),
                    'access_url': deployment_data.get('access_url')
                })
            else:
                return jsonify({
                    'status': 'error',
                    'message': result.get('message', '部署失败')
                })

        except Exception as e:
            return jsonify({
                'status': 'error',
                'message': f'部署过程中出错: {str(e)}'
            })

@bp.route('/status/<deployment_uuid>', methods=['GET'])
@login_required
def get_status(deployment_uuid):
    """获取部署状态

    Args:
        deployment_uuid: 部署UUID
    """
    if not hasattr(g, 'user') or not g.user:
        return jsonify({'status': 'error', 'message': '请先登录'})

    # 获取部署记录
    deployment = get_deployment_by_uuid(deployment_uuid)
    if not deployment:
        return jsonify({'status': 'error', 'message': '部署记录不存在'})

    # 检查权限（只能查看自己的部署或管理员）
    if deployment.get('user_id') != g.user.id and g.user.role != 'admin':
        return jsonify({'status': 'error', 'message': '没有权限查看此部署'})

    # 获取工作目录
    working_directory = deployment.get('working_directory')
    if not working_directory:
        return jsonify({'status': 'error', 'message': '部署工作目录不存在'})

    # 从Docker服务获取容器状态
    status_result = docker_service.get_container_status(deployment_uuid, working_directory)

    if status_result.get('status') == 'success':
        # 更新数据库中的状态
        container_status = status_result.get('container_status')
        if container_status and container_status != deployment.get('status'):
            update_deployment_status(deployment_uuid, container_status)
            deployment['status'] = container_status

        return jsonify({
            'status': 'success',
            'deployment': deployment
        })
    else:
        return jsonify({
            'status': 'error',
            'message': status_result.get('message', '获取状态失败')
        })



@bp.route('/logs/<deployment_uuid>', methods=['GET'])
@login_required
def get_logs(deployment_uuid):
    """获取部署日志（用于右侧实时日志面板）

    返回统一的 logs 数组，元素为 {timestamp, message}
    """
    if not hasattr(g, 'user') or not g.user:
        return jsonify({'status': 'error', 'message': '请先登录'})

    # 获取部署记录
    deployment = get_deployment_by_uuid(deployment_uuid)
    if not deployment:
        return jsonify({'status': 'error', 'message': '部署记录不存在'})

    # 检查权限（只能查看自己的部署或管理员）
    if deployment.get('user_id') != g.user.id and g.user.role != 'admin':
        return jsonify({'status': 'error', 'message': '没有权限查看此部署'})

    # 获取容器日志
    working_directory = deployment.get('working_directory')
    if not working_directory:
        return jsonify({'status': 'error', 'message': '部署工作目录不存在'})

    # get_container_logs 返回字符串，不是字典
    logs_text = docker_service.get_container_logs(deployment_uuid, working_directory, tail=200)

    # 获取状态变更日志（左侧表格用）
    db_logs = get_deployment_logs(deployment_uuid)

    # 组装统一 logs 列表
    import datetime
    combined = []

    # 容器日志按行拆分
    if logs_text and not logs_text.startswith('获取日志失败'):
        for line in logs_text.splitlines():
            if line.strip():
                combined.append({
                    'timestamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                    'message': line
                })

    # 如果没有容器日志，使用状态日志
    if not combined:
        for l in db_logs:
            ts = l.get('timestamp')
            ts_str = ts.strftime('%Y-%m-%d %H:%M:%S') if hasattr(ts, 'strftime') else str(ts)
            combined.append({
                'timestamp': ts_str,
                'message': f"[{l.get('status', '')}] {l.get('message','')}"
            })

    return jsonify({
        'status': 'success',
        'logs': combined,
        'status_logs': db_logs
    })


# EXP 验证功能已删除

# 应用检查功能已删除（依赖 AI）


@bp.route('/stop/<deployment_uuid>', methods=['POST'])
@login_required
def stop_deployment(deployment_uuid):
    """停止部署

    Args:
        deployment_uuid: 部署UUID
    """
    if not hasattr(g, 'user') or not g.user:
        return jsonify({'status': 'error', 'message': '请先登录'})

    # 获取部署记录
    deployment = get_deployment_by_uuid(deployment_uuid)
    if not deployment:
        return jsonify({'status': 'error', 'message': '部署记录不存在'})

    # 检查权限（只能操作自己的部署或管理员）
    if deployment.get('user_id') != g.user.id and g.user.role != 'admin':
        return jsonify({'status': 'error', 'message': '没有权限操作此部署'})

    # 获取工作目录
    working_directory = deployment.get('working_directory')
    if not working_directory:
        return jsonify({'status': 'error', 'message': '部署工作目录不存在'})

    # 执行停止命令
    result = docker_service.stop_deployment(deployment_uuid, working_directory)

    if result.get('status') == 'success':
        # 更新数据库状态
        update_deployment_status(deployment_uuid, 'stopped')

        return jsonify({
            'status': 'success',
            'message': '部署已停止'
        })
    else:
        return jsonify({
            'status': 'error',
            'message': result.get('message', '停止部署失败')
        })

@bp.route('/start/<deployment_uuid>', methods=['POST'])
@login_required
def start_deployment(deployment_uuid):
    """启动部署

    Args:
        deployment_uuid: 部署UUID
    """
    if not hasattr(g, 'user') or not g.user:
        return jsonify({'status': 'error', 'message': '请先登录'})

    # 获取部署记录
    deployment = get_deployment_by_uuid(deployment_uuid)
    if not deployment:
        return jsonify({'status': 'error', 'message': '部署记录不存在'})

    # 检查权限（只能操作自己的部署或管理员）
    if deployment.get('user_id') != g.user.id and g.user.role != 'admin':
        return jsonify({'status': 'error', 'message': '没有权限操作此部署'})

    # 获取工作目录
    working_directory = deployment.get('working_directory')
    if not working_directory:
        return jsonify({'status': 'error', 'message': '部署工作目录不存在'})

    # 执行启动命令
    result = docker_service.start_deployment(deployment_uuid, working_directory)

    if result.get('status') == 'success':
        # 更新数据库状态
        update_deployment_status(deployment_uuid, 'running')

        return jsonify({
            'status': 'success',
            'message': '部署已启动'
        })
    else:
        return jsonify({
            'status': 'error',
            'message': result.get('message', '启动部署失败')
        })

@bp.route('/delete/<deployment_uuid>', methods=['POST'])
@login_required
def delete_deployment(deployment_uuid):
    """删除部署

    Args:
        deployment_uuid: 部署UUID
    """
    if not hasattr(g, 'user') or not g.user:
        return jsonify({'status': 'error', 'message': '请先登录'})

    # 获取部署记录
    deployment = get_deployment_by_uuid(deployment_uuid)
    if not deployment:
        return jsonify({'status': 'error', 'message': '部署记录不存在'})

    # 检查权限（只能操作自己的部署或管理员）
    if deployment.get('user_id') != g.user.id and g.user.role != 'admin':
        return jsonify({'status': 'error', 'message': '没有权限操作此部署'})

    # 获取工作目录和端口
    working_directory = deployment.get('working_directory')
    external_port = deployment.get('external_port')

    if not working_directory:
        return jsonify({'status': 'error', 'message': '部署工作目录不存在'})

    # 执行删除
    result = docker_service.delete_deployment(deployment_uuid, working_directory, external_port)

    if result.get('status') == 'success':
        # 从数据库中完全删除记录
        delete_deployment_record(deployment_uuid)

        return jsonify({
            'status': 'success',
            'message': '部署已删除'
        })
    else:
        return jsonify({
            'status': 'error',
            'message': result.get('message', '删除部署失败')
        })

@bp.route('/view/<deployment_uuid>', methods=['GET'])
@login_required
def view_deployment(deployment_uuid):
    """查看部署详情

    Args:
        deployment_uuid: 部署UUID
    """
    if not hasattr(g, 'user') or not g.user:
        return redirect(url_for('auth.login'))

    # 获取部署记录
    deployment = get_deployment_by_uuid(deployment_uuid)
    if not deployment:
        flash('部署记录不存在', 'error')
        return redirect(url_for('deployment.index'))

    # 检查权限（只能查看自己的部署或管理员）
    if deployment.get('user_id') != g.user.id and g.user.role != 'admin':
        flash('没有权限查看此部署', 'error')
        return redirect(url_for('deployment.index'))

    # 获取状态日志
    logs = get_deployment_logs(deployment_uuid)

    # 获取容器状态
    working_directory = deployment.get('working_directory')
    if working_directory:
        status_result = docker_service.get_container_status(deployment_uuid, working_directory)
        if status_result.get('status') == 'success':
            container_status = status_result.get('container_status')
            if container_status and container_status != deployment.get('status'):
                update_deployment_status(deployment_uuid, container_status)
                deployment['status'] = container_status

    # 获取题目信息
    challenge = None
    if deployment.get('challenge_id'):
        challenge = get_challenge_record(deployment.get('challenge_id'))

    # 补充端口映射和访问地址信息
    try:
        wd = deployment.get('working_directory')
        ports = _extract_port_mappings(wd) if wd else []
        if ports:
            # 去重端口映射
            unique_ports = []
            seen_mappings = set()
            for h, ext, inter in ports:
                mapping_key = (h, ext, inter)
                if mapping_key not in seen_mappings:
                    seen_mappings.add(mapping_key)
                    unique_ports.append((h, ext, inter))

            deployment['port_mappings'] = unique_ports
            # 优先从X-Forwarded-Host获取(代理/负载均衡场景),否则使用request.host或环境变量
            host = request.headers.get('X-Forwarded-Host') or request.host.split(':')[0] or os.environ.get('HOST_ADDRESS', 'localhost')
            urls = []
            for h, ext, inter in unique_ports:
                if inter in (80, 8080, 8000, 5000, 443, 3000):
                    scheme = 'https' if inter == 443 else 'http'
                    urls.append(f"{scheme}://{host}:{ext}")
            deployment['access_urls'] = urls
        elif deployment.get('external_port'):
            # 优先从X-Forwarded-Host获取(代理/负载均衡场景),否则使用request.host或环境变量
            host = request.headers.get('X-Forwarded-Host') or request.host.split(':')[0] or os.environ.get('HOST_ADDRESS', 'localhost')
            deployment['access_urls'] = [f"http://{host}:{deployment.get('external_port')}"]
    except Exception as e:
        print(f"处理端口映射时出错: {str(e)}")
        pass

    # 读取检查脚本（若存在于部署工作目录）
    check_script_content = None
    try:
        wd = deployment.get('working_directory')
        if wd and os.path.isdir(wd):
            candidates = [
                os.path.join(wd, 'Checker', 'atta', 'web1.py'),
                os.path.join(wd, 'Checker', 'items', 'web1.py'),
            ]
            for p in candidates:
                if os.path.exists(p):
                    with open(p, 'r', encoding='utf-8', errors='ignore') as rf:
                        check_script_content = rf.read()
                    break
    except Exception:
        pass

    return render_template(
        'pages/deployment/view.html',
        deployment=deployment,
        logs=logs,
        challenge=challenge,
        check_script_content=check_script_content
    )

# 修复差异和回写功能已删除（依赖 AI）

# 文件结构和文件管理功能已删除
