import os
import subprocess
import uuid
import yaml
import paramiko
from flask import Flask, jsonify, request, render_template
from flask_cors import CORS
from flask_jwt_extended import (
    JWTManager, create_access_token,
    jwt_required, get_jwt_identity
)
from passlib.hash import pbkdf2_sha256
from logging.handlers import RotatingFileHandler
import json
from datetime import datetime, timedelta
import random
import logging
import string
import user_agents
import traceback
from werkzeug.exceptions import BadRequest
from jinja2 import Template

SSH_CONFIG = {
    'hostname': '78.46.62.242',
    'port': 22,
    'username': 'root',
    'password': '',  # Ensure this is securely stored and handled
    # 'key_filepath': '/path/to/your/private/key',  # Remove or keep commented out
}

app = Flask(__name__, static_folder='static', template_folder='templates')
CORS(app)

# JWT Configuration
app.config['JWT_SECRET_KEY'] = 'gbauy45bguyab3e4gbaehrbghbg'  # Измените в продакшене
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
jwt = JWTManager(app)

OS_OPTIONS = {
    'win2020': 'Windows Server 2020',
    'win2022': 'Windows Server 2022',
    'win2025': 'Windows Server 2025',
    'win10': 'Windows 10',
    'win11': 'Windows 11'
}

IP_POOL_START = [192, 168, 1, 100]  # Starting IP address
current_ip = IP_POOL_START.copy()

CONTAINERS_DIR = os.path.join(os.getcwd(), 'containers')
TEMPLATES_DIR = os.path.join(CONTAINERS_DIR, 'templates')
os.makedirs(TEMPLATES_DIR, exist_ok=True)

# Ensure the Docker Compose template exists
TEMPLATE_PATH = os.path.join(TEMPLATES_DIR, 'docker-compose-template.yml')
if not os.path.exists(TEMPLATE_PATH):
    with open(TEMPLATE_PATH, 'w') as f:
        f.write("""version: '3'
services:
  windows:
    image: dockurr/windows  # Ensure this image exists or adjust to the correct one
    container_name: {{ container_name }}
    environment:
      VERSION: "{{ version }}"
      DISK_SIZE: "{{ disk_size }}"
      RAM_SIZE: "{{ ram_size }}"
      CPU_CORES: "{{ cpu_cores }}"
      USERNAME: "{{ username }}"
      PASSWORD: "{{ password }}"
      KEYBOARD: "{{ keyboard }}"
    devices:
      - /dev/kvm
    cap_add:
      - NET_ADMIN
    ports:
      - "{{ public_port_8006 }}:8006/tcp"
      - "{{ public_port_rdp_tcp }}:3389/tcp"
      - "{{ public_port_rdp_udp }}:3389/udp"
    stop_grace_period: 2m
    restart: unless-stopped
""")

def generate_unique_ip():
    global current_ip
    ip = '.'.join(map(str, current_ip))
    current_ip[3] += 1
    if current_ip[3] > 254:
        current_ip[3] = 1
        current_ip[2] += 1
        if current_ip[2] > 254:
            current_ip[2] = 1
            current_ip[1] += 1
            if current_ip[1] > 254:
                current_ip[1] = 1
                current_ip[0] += 1
                if current_ip[0] > 254:
                    raise Exception("IP Pool Exhausted")
    return ip

@app.route('/')
def home():
    return render_template('index.html')

def setup_logging():
    # Create logs directory if it doesn't exist
    if not os.path.exists('logs'):
        os.makedirs('logs')
    
    # Configure main application logger
    formatter = logging.Formatter(
        '[%(asctime)s] %(levelname)s in %(module)s: %(message)s'
    )
    
    # File handler for general logs
    file_handler = RotatingFileHandler(
        'logs/app.log', 
        maxBytes=1024 * 1024,  # 1MB
        backupCount=10
    )
    file_handler.setFormatter(formatter)
    file_handler.setLevel(logging.INFO)
    
    # File handler for errors
    error_file_handler = RotatingFileHandler(
        'logs/error.log',
        maxBytes=1024 * 1024,  # 1MB
        backupCount=10
    )
    error_file_handler.setFormatter(formatter)
    error_file_handler.setLevel(logging.ERROR)
    
    # Configure Flask logger
    app.logger.addHandler(file_handler)
    app.logger.addHandler(error_file_handler)
    app.logger.setLevel(logging.INFO)
    
    # Log basic request info
    @app.before_request
    def log_request_info():
        app.logger.info(f'Request: {request.method} {request.url} from {request.remote_addr}')
        app.logger.debug(f'Headers: {dict(request.headers)}')
    
    # Error handler
    @app.errorhandler(Exception)
    def handle_exception(e):
        app.logger.error(f'Unhandled Exception: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({
            'error': 'An internal server error occurred',
            'details': str(e) if app.debug else None
        }), 500

def load_db():
    try:
        if not os.path.exists('db.json'):
            with open('db.json', 'w') as f:
                json.dump({'users': {}, 'server_plans': [], 'logs': []}, f)
        with open('db.json', 'r') as f:
            db = json.load(f)
        return db
    except Exception as e:
        app.logger.error(f'Database load error: {str(e)}')
        app.logger.error(traceback.format_exc())
        raise

def save_db(db):
    with open('db.json', 'w') as f:
        json.dump(db, f, indent=2)

def log_activity(username, action, user_agent_string):
    try:
        db = load_db()
        ua = user_agents.parse(user_agent_string)
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'username': username,
            'action': action,
            'browser': ua.browser.family,
            'os': ua.os.family,
            'device': ua.device.family,
            'ip': request.remote_addr
        }
        db['logs'].append(log_entry)
        save_db(db)
    except Exception as e:
        app.logger.error(f'Error logging activity: {str(e)}')
        app.logger.error(traceback.format_exc())

def generate_password():
    return ''.join(random.choices(string.ascii_letters + string.digits, k=12))

def execute_ssh_command(command):
    try:
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        
        connect_params = {
            'hostname': SSH_CONFIG['hostname'],
            'port': SSH_CONFIG.get('port', 22),
            'username': SSH_CONFIG['username']
        }
        
        if 'key_filepath' in SSH_CONFIG and SSH_CONFIG['key_filepath']:
            connect_params['key_filename'] = SSH_CONFIG['key_filepath']
        elif 'password' in SSH_CONFIG and SSH_CONFIG['password']:
            connect_params['password'] = SSH_CONFIG['password']
        else:
            raise ValueError("Either 'key_filepath' or 'password' must be provided in SSH_CONFIG.")
        
        ssh.connect(**connect_params)
        
        stdin, stdout, stderr = ssh.exec_command(command)
        stdout_str = stdout.read().decode()
        stderr_str = stderr.read().decode()
        ssh.close()
        return stdout_str, stderr_str
    except Exception as e:
        app.logger.error(f'SSH command execution error: {e}')
        return None, str(e)

def create_docker_compose_content(container_name, version, plan_details, rdp_username, rdp_password, public_port_8006, public_port_rdp_tcp, public_port_rdp_udp):
    return f"""
    version: '3'
    services:
      windows:
        image: dockurr/windows
        container_name: {container_name}
        environment:
          VERSION: "{version}"
          DISK_SIZE: "{plan_details['DISK']}"
          RAM_SIZE: "{plan_details['RAM']}"
          CPU_CORES: "{plan_details['CPU']}"
          USERNAME: "{rdp_username}"
          PASSWORD: "{rdp_password}"
        devices:
          - /dev/kvm
        cap_add:
          - NET_ADMIN
        ports:
          - "{public_port_8006}:8006/tcp"
          - "{public_port_rdp_tcp}:3389/tcp"
          - "{public_port_rdp_udp}:3389/udp"
        stop_grace_period: 2m
        restart: unless-stopped
    """
  
def connect_via_ssh():
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    connect_params = {
        'hostname': SSH_CONFIG['hostname'],
        'port': SSH_CONFIG.get('port', 22),
        'username': SSH_CONFIG['username'],
        'password': SSH_CONFIG['password']
    }
    try:
        ssh.connect(**connect_params)
    except Exception as e:
        app.logger.error(f'SSH connection failed: {e}')
        return None
    return ssh

@app.route('/api/servers', methods=['POST'])
@jwt_required()
def servers():
    current_user = get_jwt_identity()
    db = load_db()

    data = request.get_json()
    if not data or 'plan' not in data or 'os' not in data or 'name' not in data:
        app.logger.error('Invalid request data')
        return jsonify({'error': 'Invalid request data'}), 400

    # Retrieve plan details, ensuring the plan.name is compared correctly
    plan_name = data['plan']['name']
    selected_plan = next((p for p in db['server_plans'] if p['name'] == plan_name), None)
    if not selected_plan:
        app.logger.error('Invalid plan selected')
    return jsonify({'error': 'Invalid plan selected'}), 400

    if user_data['balance'] < selected_plan['price']:
            return jsonify({'error': 'Insufficient funds'}), 400

    if data['os'] not in OS_OPTIONS:
            return jsonify({'error': 'Invalid operating system selected'}), 400

        # Generate unique properties for the new server
    server_ip = generate_unique_ip()
    container_name = f"server_{uuid.uuid4().hex[:8]}"
    rdp_password = generate_password()
    public_port_8006 = random.randint(10000, 60000)
    public_port_rdp_tcp = random.randint(10000, 60000)
    public_port_rdp_udp = public_port_rdp_tcp

    # Generate Docker Compose content
    docker_compose_content = create_docker_compose_content(
        container_name, data['os'], selected_plan,
        data.get('rdp_username', 'admin'), rdp_password,
        public_port_8006, public_port_rdp_tcp, public_port_rdp_udp
    )

    # Create server directory
    server_dir = os.path.join(CONTAINERS_DIR, container_name)
    os.makedirs(server_dir, exist_ok=True)
    docker_compose_path = os.path.join(server_dir, 'docker-compose.yml')
    with open(docker_compose_path, 'w') as f:
            f.write(docker_compose_content)

        # Connect via SSH and deploy the server
        ssh_client = connect_via_ssh()
        if not ssh_client:
            return jsonify({'error': 'Failed to establish SSH connection'}), 500

        try:
            sftp = ssh_client.open_sftp()
            remote_path = f"/root/{container_name}"
            sftp.mkdir(remote_path)
            sftp.put(docker_compose_path, os.path.join(remote_path, 'docker-compose.yml'))
            sftp.close()

            commands = [
                f"cd {remote_path}",
                f"docker-compose up -d"
            ]
            stdout, stderr = execute_ssh_command(' && '.join(commands))
            if stderr:
                app.logger.error(f'Error executing remote commands: {stderr}')
                raise Exception(f'Remote command error: {stderr}')
        except Exception as e:
            app.logger.error(f'Error during SSH operation: {e}')
            return jsonify({'error': f'Failed to deploy server: {str(e)}'}), 500
        finally:
            ssh_client.close()

        # Register the new server
        new_server = {
            'id': len(user_data['servers']) + 1,
            'name': data['name'],
            'plan': selected_plan,
            'os': OS_OPTIONS[data['os']],
            'status': 'active',
            'ip': server_ip,
            'created_at': datetime.now().isoformat(),
            'container_name': container_name,
            'ports': {
                '8006/tcp': public_port_8006,
                '3389/tcp': public_port_rdp_tcp,
                '3389/udp': public_port_rdp_udp
            }
        }

        user_data['balance'] -= selected_plan['price']
        user_data['servers'].append(new_server)
        save_db(db)
        log_activity(current_user, 'create_server', request.headers.get('User-Agent', ''))
        return jsonify(new_server), 201


@app.route('/api/login', methods=['POST'])
def login():
    try:
        app.logger.info('Processing login request')
        db = load_db()
        data = request.get_json()
        
        if not data:
            app.logger.warning('No JSON data in login request')
            return jsonify({'error': 'No JSON data provided'}), 400
        
        username = data.get('username')
        password = data.get('password')
        
        app.logger.info(f'Login attempt for user: {username}')
        
        if not username or not password:
            app.logger.warning('Missing username or password in login request')
            return jsonify({'error': 'Username and password are required'}), 400
        
        user_data = db['users'].get(username)
        if not user_data:
            app.logger.warning(f'Login failed: User {username} not found')
            return jsonify({'error': 'Invalid credentials'}), 401
            
        if pbkdf2_sha256.verify(password, user_data['password']):
            app.logger.info(f'Login successful for user: {username}')
            access_token = create_access_token(identity=username)
            try:
                log_activity(username, 'login', request.headers.get('User-Agent', ''))
            except Exception as e:
                app.logger.error(f'Failed to log login activity: {str(e)}')
                # Continue with login even if logging fails
            return jsonify({
                'token': access_token,
                'username': username,
                'balance': user_data.get('balance', 0)
            })
        
        app.logger.warning(f'Login failed: Invalid password for user {username}')
        return jsonify({'error': 'Invalid credentials'}), 401
        
    except Exception as e:
        app.logger.error(f'Login error: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500

@app.route('/api/register', methods=['POST'])
def register():
    try:
        app.logger.info('Processing registration request')
        db = load_db()
        data = request.get_json()
        
        if not data:
            app.logger.warning('No JSON data in registration request')
            return jsonify({'error': 'No JSON data provided'}), 400
        
        username = data.get('username')
        password = data.get('password')
        
        app.logger.info(f'Registration attempt for username: {username}')
        
        if not username or not password:
            app.logger.warning('Missing username or password in registration request')
            return jsonify({'error': 'Username and password are required'}), 400
        
        if username in db['users']:
            app.logger.warning(f'Registration failed: Username {username} already exists')
            return jsonify({'error': 'Username already taken'}), 400
        
        hashed_password = pbkdf2_sha256.hash(password)
        
        db['users'][username] = {
            'password': hashed_password,
            'servers': [],
            'balance': 0,
            'tickets': [],
            'created_at': datetime.now().isoformat()
        }
        
        save_db(db)
        app.logger.info(f'Registration successful for user: {username}')
        log_activity(username, 'register', request.headers.get('User-Agent', ''))
        return jsonify({'message': 'Registration successful'}), 201
        
    except Exception as e:
        app.logger.error(f'Registration error: {str(e)}')
        app.logger.error(traceback.format_exc())
        return jsonify({'error': 'An internal server error occurred'}), 500


@app.route('/api/servers/<int:server_id>', methods=['GET'])
@jwt_required()
def get_server(server_id):
    current_user = get_jwt_identity()
    db = load_db()
    user_data = db['users'].get(current_user)

    if not user_data:
        return jsonify({'error': 'User not found'}), 404

    server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
    if not server:
        return jsonify({'error': 'Server not found'}), 404

    return jsonify(server), 200

@app.route('/api/servers/<int:server_id>/toggle', methods=['POST'])
@jwt_required()
def toggle_server(server_id):
    current_user = get_jwt_identity()
    db = load_db()
    user_data = db['users'].get(current_user)

    if not user_data:
        return jsonify({'error': 'User not found'}), 404

    server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
    if not server:
        return jsonify({'error': 'Server not found'}), 404

    container_name = server.get('container_name')
    if not container_name:
        return jsonify({'error': 'Container name not found for this server'}), 400

    # Определение команды для остановки или запуска контейнера
    if server['status'] == 'active':
        cmd = f"cd {os.path.join(CONTAINERS_DIR, container_name)} && docker-compose stop"
    else:
        cmd = f"cd {os.path.join(CONTAINERS_DIR, container_name)} && docker-compose start"
    
    stdout, stderr = execute_ssh_command(cmd)
    if stderr:
        app.logger.error(f'SSH command error: {stderr}')
        return jsonify({'error': f'Failed to toggle server status: {stderr}'}), 500
    
    # Обновление статуса
    server['status'] = 'frozen' if server['status'] == 'active' else 'active'
    save_db(db)
    log_activity(current_user, f'toggle_server_{server_id}', request.headers.get('User-Agent', ''))
    return jsonify({'message': f"Server status changed to {server['status']}"}), 200

@app.route('/api/servers/<int:server_id>', methods=['DELETE'])
@jwt_required()
def delete_server(server_id):
    current_user = get_jwt_identity()
    db = load_db()
    user_data = db['users'].get(current_user)

    if not user_data:
        return jsonify({'error': 'User not found'}), 404

    server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
    if not server:
        return jsonify({'error': 'Server not found'}), 404

    container_name = server.get('container_name')
    if not container_name:
        return jsonify({'error': 'Container name not found for this server'}), 400

    # Команды для остановки и удаления контейнера на удалённом сервере
    commands = [
        f"cd {os.path.join(CONTAINERS_DIR, container_name)} && docker-compose down",
        f"rm -rf {os.path.join(CONTAINERS_DIR, container_name)}"
    ]
    full_command = ' && '.join(commands)
    
    stdout, stderr = execute_ssh_command(full_command)
    if stderr:
        app.logger.error(f'SSH command error: {stderr}')
        return jsonify({'error': f'Failed to delete server: {stderr}'}), 500

    # Удаление локальных архива (если есть)
    zip_filename = f"{container_name}.tar.gz"
    if os.path.exists(zip_filename):
        os.remove(zip_filename)
    
    # Удаление сервера из базы данных
    user_data['servers'].remove(server)
    save_db(db)
    log_activity(current_user, f'delete_server_{server_id}', request.headers.get('User-Agent', ''))
    return jsonify({'message': 'Server deleted successfully'}), 200

@app.route('/api/servers/<int:server_id>', methods=['PUT'])
@jwt_required()
def update_server(server_id):
    current_user = get_jwt_identity()
    db = load_db()
    user_data = db['users'].get(current_user)

    if not user_data:
        return jsonify({'error': 'User not found'}), 404

    server = next((s for s in user_data['servers'] if s['id'] == server_id), None)
    if not server:
        return jsonify({'error': 'Server not found'}), 404

    data = request.get_json()
    if not data:
        return jsonify({'error': 'No data provided'}), 400

    # Example: Allow updating server name and OS
    new_name = data.get('name')
    new_os = data.get('os')

    if new_name:
        server['name'] = new_name

    if new_os:
        if new_os not in OS_OPTIONS:
            return jsonify({'error': 'Invalid operating system selected'}), 400
        # Обновление Docker Compose файла
        container_name = server.get('container_name')
        server_dir = os.path.join(CONTAINERS_DIR, container_name)
        docker_compose_path = os.path.join(server_dir, 'docker-compose.yml')
        
        if not os.path.exists(docker_compose_path):
            return jsonify({'error': 'docker-compose.yml not found for this server'}), 400
        
        # Загрузка текущего docker-compose.yml
        try:
            with open(docker_compose_path, 'r') as f:
                compose_data = yaml.safe_load(f)
        except Exception as e:
            app.logger.error(f'Error reading docker-compose.yml: {e}')
            return jsonify({'error': 'Failed to read docker-compose.yml'}), 500
        
        # Обновление версии ОС
        compose_data['services']['windows']['environment']['VERSION'] = new_os
        compose_data['services']['windows']['os'] = OS_OPTIONS[new_os]
        
        # Запись обновленного docker-compose.yml
        try:
            with open(docker_compose_path, 'w') as f:
                yaml.dump(compose_data, f)
        except Exception as e:
            app.logger.error(f'Error writing docker-compose.yml: {e}')
            return jsonify({'error': 'Failed to update docker-compose.yml'}), 500
        
        # Перезапуск контейнера на удалённом сервере для применения изменений
        cmd = f"cd {server_dir} && docker-compose up -d"
        stdout, stderr = execute_ssh_command(cmd)
        if stderr:
            app.logger.error(f'SSH command error: {stderr}')
            return jsonify({'error': f'Failed to restart server: {stderr}'}), 500

        # Обновление поля ОС в базе данных
        server['os'] = OS_OPTIONS[new_os]

    save_db(db)
    log_activity(current_user, f'update_server_{server_id}', request.headers.get('User-Agent', ''))
    return jsonify({'message': 'Server updated successfully', 'server': server}), 200

@app.route('/api/userinfo', methods=['GET', 'PUT'])
@jwt_required()
def userinfo():
    current_user = get_jwt_identity()
    db = load_db()
    user_data = db['users'].get(current_user)

    if not user_data:
        return jsonify({'error': 'User not found'}), 404

    if request.method == 'GET':
        return jsonify({'username': current_user, 'balance': user_data.get('balance', 0)}), 200
    
    elif request.method == 'PUT':
        data = request.get_json()
        if not data:
            return jsonify({'error': 'No data provided'}), 400
        
        current_password = data.get('current_password')
        new_username = data.get('new_username')
        new_password = data.get('new_password')

        if not current_password:
            return jsonify({'error': 'Current password is required'}), 400

        # Verify current password
        if not pbkdf2_sha256.verify(current_password, user_data['password']):
            return jsonify({'error': 'Current password is incorrect'}), 401

        # Update username if provided
        if new_username:
            if new_username in db['users']:
                return jsonify({'error': 'Username already taken'}), 400
            db['users'][new_username] = db['users'].pop(current_user)
            current_user = new_username

        # Update password if provided
        if new_password:
            db['users'][current_user]['password'] = pbkdf2_sha256.hash(new_password)

        save_db(db)
        log_activity(current_user, 'update_userinfo', request.headers.get('User-Agent', ''))
        return jsonify({'message': 'User information updated successfully', 'username': current_user}), 200

@app.route('/api/tickets', methods=['GET', 'POST'])
@jwt_required()
def tickets():
    current_user = get_jwt_identity()
    db = load_db()
    
    if request.method == 'GET':
        user_data = db['users'].get(current_user)
        if not user_data:
            return jsonify({'error': 'User not found'}), 404
            
        return jsonify(user_data.get('tickets', []))
    
    elif request.method == 'POST':
        data = request.get_json()
        if not data or 'title' not in data or 'description' not in data:
            return jsonify({'error': 'Invalid request data'}), 400
            
        user_data = db['users'].get(current_user)
        if not user_data:
            return jsonify({'error': 'User not found'}), 404
            
        new_ticket = {
            'id': len(user_data['tickets']) + 1,
            'title': data['title'],
            'description': data.get('description', ''),
            'status': 'open',
            'created_at': datetime.now().isoformat(),
            'messages': []
        }
        
        user_data['tickets'].append(new_ticket)
        save_db(db)
        log_activity(current_user, 'create_ticket', request.headers.get('User-Agent', ''))
        return jsonify(new_ticket), 201

@app.route('/api/tickets/<int:ticket_id>', methods=['GET'])
@jwt_required()
def get_ticket(ticket_id):
    current_user = get_jwt_identity()
    db = load_db()
    user_data = db['users'].get(current_user)

    if not user_data:
        return jsonify({'error': 'User not found'}), 404

    ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
    if not ticket:
        return jsonify({'error': 'Ticket not found'}), 404

    return jsonify(ticket), 200

@app.route('/api/tickets/<int:ticket_id>/reply', methods=['POST'])
@jwt_required()
def reply_ticket(ticket_id):
    current_user = get_jwt_identity()
    db = load_db()
    
    user_data = db['users'].get(current_user)
    if not user_data:
        return jsonify({'error': 'User not found'}), 404
        
    ticket = next((t for t in user_data['tickets'] if t['id'] == ticket_id), None)
    if not ticket:
        return jsonify({'error': 'Ticket not found'}), 404
        
    data = request.get_json()
    if not data or 'message' not in data:
        return jsonify({'error': 'Message is required'}), 400
        
    message = {
        'content': data['message'],
        'sender': current_user,
        'timestamp': datetime.now().isoformat()
    }
    
    ticket['messages'].append(message)
    save_db(db)
    log_activity(current_user, f'reply_ticket_{ticket_id}', request.headers.get('User-Agent', ''))
    return jsonify(message), 201

if __name__ == '__main__':
    setup_logging()
    app.run(debug=True)



fix my code psppslsp