#!/usr/bin/env python3
"""
GitHub Webhook Handler for auto-deploying SREBOT on main branch pushes.
Listens for push events, pulls changes, and restarts pm2.
"""

import hmac
import hashlib
import logging
import subprocess
import os
from flask import Flask, request, jsonify
from dotenv import load_dotenv

load_dotenv()

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)

app = Flask(__name__)

WEBHOOK_SECRET = os.environ.get('GITHUB_WEBHOOK_SECRET', '')
REPO_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
WEBHOOK_FILENAME = 'SREBOT/github_webhook_updater.py'


def verify_signature(payload: bytes, signature: str) -> bool:
    """Verify GitHub webhook signature."""
    if not WEBHOOK_SECRET:
        logger.warning("No webhook secret configured")
        return False

    if not signature:
        logger.warning("No signature provided in request")
        return False

    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)


def pull_and_restart(changed_files: list[str], before: str = '', after: str = '') -> tuple[bool, str]:
    """Pull latest changes from main and restart pm2 processes."""
    try:
        # Change to repo directory
        os.chdir(REPO_PATH)
        logger.info(f"Changed to repo directory: {REPO_PATH}")

        # Fetch and pull main branch
        logger.info("Fetching from origin/main...")
        subprocess.run(
            ['git', 'fetch', 'origin', 'main'],
            capture_output=True, text=True, timeout=30
        )

        logger.info("Pulling from origin/main...")
        pull_result = subprocess.run(
            ['git', 'pull', 'origin', 'main'],
            capture_output=True, text=True, timeout=60
        )

        if pull_result.returncode != 0:
            logger.error(f"Git pull failed: {pull_result.stderr}")
            return False, f"Git pull failed: {pull_result.stderr}"

        # Check if there were actual changes
        if 'Already up to date' in pull_result.stdout:
            logger.info("Already up to date, no restart needed")
            return True, "Already up to date, no restart needed"

        logger.info(f"Pull successful: {pull_result.stdout.strip()}")

        # If the payload commits list was empty (e.g. GitHub PR merge), derive
        # changed files from the before/after SHAs instead.
        if not changed_files and before and after and before != '0' * 40:
            diff_result = subprocess.run(
                ['git', 'diff', '--name-only', before, after],
                capture_output=True, text=True, timeout=15
            )
            if diff_result.returncode == 0:
                changed_files = [f for f in diff_result.stdout.splitlines() if f]
                logger.info(f"Derived changed files from git diff: {changed_files}")

        # Determine which processes to restart based on changed files
        processes_to_restart = []

        # SREBOT bot: restart if SREBOT/BOT/, SHARED/, SREBOT/start_bot.py,
        # SREBOT/ecosystem.config.js, or SREBOT root .py files changed.
        srebot_changed = any(
            f.startswith('SREBOT/BOT/')
            or f.startswith('SHARED/')
            or f == 'SREBOT/start_bot.py'
            or f == 'SREBOT/ecosystem.config.js'
            or (f.startswith('SREBOT/') and f.endswith('.py') and f.count('/') == 1 and not f.endswith('/' + WEBHOOK_FILENAME))
            for f in changed_files
        )
        if srebot_changed:
            processes_to_restart.append('srebot')
            logger.info("SREBOT files changed, will restart srebot")

        # TSSBOT bot: restart if TSSBOT/BOT/, TSSBOT/start_bot.py,
        # TSSBOT/ecosystem.config.js, SHARED/, or TSSBOT root .py files changed.
        tssbot_changed = any(
            f.startswith('TSSBOT/BOT/')
            or f.startswith('SHARED/')
            or f == 'TSSBOT/start_bot.py'
            or f == 'TSSBOT/ecosystem.config.js'
            or (f.startswith('TSSBOT/') and f.endswith('.py') and f.count('/') == 1)
            for f in changed_files
        )
        if tssbot_changed:
            processes_to_restart.append('tssbot')
            logger.info("TSSBOT files changed, will restart tssbot")

        # API server: restart if SREBOT/server.js changed
        api_changed = any(f == 'SREBOT/server.js' for f in changed_files)
        if api_changed:
            processes_to_restart.append('srebot-api')
            logger.info("API server.js changed, will restart srebot-api")

        # Web frontend: restart if SREBOT/web/ files changed
        web_changed = any(f.startswith('SREBOT/web/') for f in changed_files)
        if web_changed:
            # Rebuild CSS if tailwind source or config changed
            css_changed = any(
                f in ('SREBOT/web/public/css/tailwind.css', 'SREBOT/web/tailwind.config.js')
                for f in changed_files
            )
            if css_changed:
                logger.info("Tailwind source changed, rebuilding CSS...")
                build_result = subprocess.run(
                    ['npm', 'run', 'build:css'],
                    capture_output=True, text=True, timeout=60,
                    cwd=os.path.join(REPO_PATH, 'SREBOT', 'web')
                )
                if build_result.returncode != 0:
                    logger.error(f"CSS build failed: {build_result.stderr}")
                    return False, f"CSS build failed: {build_result.stderr}"
                logger.info("CSS build successful")

            # Rebuild obfuscated JS if any JS source files changed
            js_changed = any(
                f.startswith('SREBOT/web/public/js/') and f.endswith('.js') and '/dist/' not in f
                for f in changed_files
            )
            if js_changed:
                logger.info("JS source files changed, rebuilding obfuscated JS...")
                build_result = subprocess.run(
                    ['node', 'build.js'],
                    capture_output=True, text=True, timeout=120,
                    cwd=os.path.join(REPO_PATH, 'SREBOT', 'web')
                )
                if build_result.returncode != 0:
                    logger.warning(f"JS build failed (non-fatal): {build_result.stderr}")
                    logger.warning("Continuing deploy — unobfuscated JS will be served as fallback")
                else:
                    logger.info("JS build successful")

            processes_to_restart.append('srebot-web')
            logger.info("Web files changed, will restart srebot-web")

        # Webhook: restart if this file changed
        webhook_changed = any(WEBHOOK_FILENAME in f for f in changed_files)
        if webhook_changed:
            processes_to_restart.append('srebot-webhook')
            logger.info(f"Webhook file changed, will restart srebot-webhook")

        if not processes_to_restart:
            logger.info(f"No relevant process files changed, skipping restart. Files: {changed_files}")
            return True, "Pulled but no restart needed"

        # Flush pm2 logs before restarting to free disk space
        logger.info("Flushing PM2 logs...")
        subprocess.run(
            ['pm2', 'flush'],
            capture_output=True, text=True, timeout=30
        )

        # Restart pm2 processes
        restarted = []
        for process in processes_to_restart:
            logger.info(f"Restarting PM2 process: {process}")
            restart_result = subprocess.run(
                ['pm2', 'restart', process],
                capture_output=True, text=True, timeout=30
            )
            if restart_result.returncode != 0:
                logger.error(f"PM2 restart failed for {process}: {restart_result.stderr}")
                return False, f"PM2 restart failed for {process}: {restart_result.stderr}"
            restarted.append(process)

        logger.info(f"PM2 restart successful: {restarted}")
        return True, f"Pulled and restarted {restarted}"

    except subprocess.TimeoutExpired:
        logger.error("Command timed out")
        return False, "Command timed out"
    except Exception as e:
        logger.exception(f"Unexpected error during pull_and_restart: {e}")
        return False, f"Error: {str(e)}"


@app.route('/webhook', methods=['POST'])
def webhook():
    """Handle GitHub webhook POST requests."""
    logger.info(f"Received webhook request from {request.remote_addr}")

    # Verify signature
    signature = request.headers.get('X-Hub-Signature-256', '')
    if not verify_signature(request.data, signature):
        logger.warning(f"Invalid signature from {request.remote_addr}")
        return jsonify({'error': 'Invalid signature'}), 403

    # Parse event type
    event = request.headers.get('X-GitHub-Event', '')
    logger.info(f"GitHub event type: {event}")

    if event == 'ping':
        logger.info("Received ping event, responding with pong")
        return jsonify({'message': 'pong'}), 200

    if event != 'push':
        logger.info(f"Ignoring non-push event: {event}")
        return jsonify({'message': f'Ignoring event: {event}'}), 200

    # Parse JSON payload with error handling
    try:
        payload = request.get_json()
        if payload is None:
            logger.error("Failed to parse JSON payload (returned None)")
            return jsonify({'error': 'Invalid JSON payload'}), 400
    except Exception as e:
        logger.error(f"Failed to parse JSON payload: {e}")
        return jsonify({'error': 'Invalid JSON payload'}), 400

    # Check if it's the main branch
    ref = payload.get('ref', '')
    logger.info(f"Push to ref: {ref}")

    if ref != 'refs/heads/main':
        logger.info(f"Ignoring push to non-main branch: {ref}")
        return jsonify({'message': f'Ignoring branch: {ref}'}), 200

    # Extract changed files from commits
    commits = payload.get('commits', [])
    pusher = payload.get('pusher', {}).get('name', 'unknown')
    before = payload.get('before', '')
    after = payload.get('after', '')
    logger.info(f"Push from {pusher} with {len(commits)} commit(s)")

    changed_files = []
    for commit in commits:
        changed_files.extend(commit.get('added', []))
        changed_files.extend(commit.get('modified', []))
        changed_files.extend(commit.get('removed', []))

    logger.info(f"Changed files: {changed_files}")

    # Pull and restart
    success, message = pull_and_restart(changed_files, before, after)

    if success:
        logger.info(f"Webhook update successful: {message}")
        return jsonify({'success': True, 'message': message}), 200
    else:
        logger.error(f"Webhook update failed: {message}")
        return jsonify({'success': False, 'error': message}), 500


@app.route('/health', methods=['GET'])
def health():
    """Health check endpoint."""
    return jsonify({'status': 'ok'}), 200


if __name__ == '__main__':
    port = int(os.environ.get('SREBOT_WEBHOOK_PORT', 9000))
    logger.info(f"Starting webhook server on port {port}")
    logger.info(f"Repo path: {REPO_PATH}")
    app.run(host='0.0.0.0', port=port)
