#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
bunny2fmc CLI: Main entry point with argparse, credential management, and logging
"""

import argparse
import getpass
import logging
import sys
import os
import subprocess
import shutil
from logging.handlers import RotatingFileHandler

import bunny2fmc
from bunny2fmc.config import CredentialManager, ConfigManager
from bunny2fmc.sync_engine import sync
# Global epilog text for help output
_EPILOG = """
Examples:

  bunny2fmc --setup        # First time setup (interactive)
  bunny2fmc --run          # Run sync once
  bunny2fmc --config       # Show current configuration
  bunny2fmc --logs         # View last 20 lines of log
  bunny2fmc --logs follow  # Follow log in realtime
  bunny2fmc --start        # Start scheduled syncs
  bunny2fmc --stop         # Stop scheduled syncs
  bunny2fmc --init         # Copy docs (QUICK_START.md etc.) to current dir
  bunny2fmc --clear        # Clear all configuration and credentials

Quick Start:

  1. Create virtual environment:
     python3 -m venv .venv

  2. Activate it:
     source .venv/bin/activate    # Linux/macOS
     .venv\\Scripts\\activate      # Windows

  3. Install bunny2fmc:
     pip install bunny2fmc

  4. Run setup wizard:
     bunny2fmc --setup

  5. Credentials are stored securely in your OS keyring

Upgrade:

  pip install --upgrade bunny2fmc

Log file: ~/.local/share/bunny2fmc/logs/bunny2fmc.log

About:

  bunny2fmc is a tool that automatically syncs the latest BunnyCDN IP address
  ranges to a Dynamic Object in Cisco FMC. It can run on-demand or on a schedule
  via cron, ensuring your security policies always have the latest CDN IPs.

  Features:
  - Automatic IP range synchronization from BunnyCDN
  - Secure credential storage in OS keyring
  - Scheduled execution via cron jobs
  - Real-time log monitoring
  - Easy on/off toggle for scheduled syncs

API User Setup (Recommended):

  To avoid being logged out of FMC during syncs, create a dedicated API user:
  
  1. In Cisco FMC, create a new user:
     Username: bunny2fmc_sync
     Authentication: Local (password)
  
  2. Assign required roles:
     ☑ Network Admin  (or)
     ☑ Maintenance User
  
  3. This user needs permissions for:
     - Object Management → Dynamic Objects: Read, Create, Modify
  
  4. Use this user in bunny2fmc --setup (not your admin account)
"""



def setup_logging():
    """Configure logging with rotating file handler"""
    logger = logging.getLogger("bunny2fmc")
    logger.setLevel(logging.INFO)

    config_dir = ConfigManager.get_config_dir()
    log_file = ConfigManager.get_log_file()

    log_format = "[%(asctime)s] %(levelname)-8s %(name)s  %(message)s"

    # Rotating file handler (10 MB per file)
    file_handler = RotatingFileHandler(
        log_file,
        maxBytes=10 * 1024 * 1024,  # 10 MB
        backupCount=5,
    )
    file_handler.setLevel(logging.INFO)
    file_handler.setFormatter(logging.Formatter(log_format))
    logger.addHandler(file_handler)

    # Console handler (for interactive mode)
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_handler.setFormatter(logging.Formatter(log_format))
    logger.addHandler(console_handler)

    return logger


def _masked_input(prompt):
    """
    Read password input with asterisks masking (works on most terminals).
    Falls back to getpass if terminal doesn't support it.
    """
    import sys
    import tty
    import termios
    
    try:
        # Try to use terminal control for masking
        sys.stdout.write(prompt)
        sys.stdout.flush()
        
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        
        password = []
        try:
            tty.setraw(fd)
            while True:
                char = sys.stdin.read(1)
                if char == '\r' or char == '\n':
                    sys.stdout.write('\n')
                    break
                elif char == '\x7f' or char == '\x08':  # Backspace
                    if password:
                        password.pop()
                        sys.stdout.write('\b \b')
                else:
                    password.append(char)
                    sys.stdout.write('*')
                sys.stdout.flush()
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        
        return ''.join(password)
    except (ImportError, OSError, termios.error):
        # Fallback to getpass if terminal control fails
        return getpass.getpass(prompt)


def _get_password(prompt):
    """
    Get password input. Uses getpass in interactive mode (TTY), uses input() in piped mode.
    This ensures compatibility with both terminal and non-interactive (piped/heredoc) input.
    """
    if sys.stdin.isatty():
        # Interactive terminal mode: use getpass for masking
        return getpass.getpass(prompt)
    else:
        # Piped/heredoc mode: use input() to consume from stdin sequentially
        sys.stdout.write(prompt)
        sys.stdout.flush()
        return sys.stdin.readline().rstrip("\n")



def interactive_setup():
    """
    Interactive setup: prompt for credentials and interval.
    Loads previous values as defaults. Password is hidden.
    Returns dict with configuration.
    """
    print("\n" + "=" * 60)
    print("bunny2fmc - Configuration")
    print("=" * 60)
    print("Enter your FMC and Bunny configuration.\n")

    # Try to load previous configuration
    try:
        previous_url = ConfigManager.load_fmc_base_url()
        previous_username = ConfigManager.load_fmc_username()
        previous_dynamic = ConfigManager.load_dynamic_object_name()
        previous_ipv6 = ConfigManager.load_include_ipv6()
        previous_interval = ConfigManager.load_sync_interval_minutes()
        # Check if password is already stored in keyring
        has_stored_password = CredentialManager.get_credentials() is not None
    except Exception:
        previous_url = None
        previous_username = None
        previous_dynamic = None
        previous_ipv6 = False
        previous_interval = None
        has_stored_password = False

    # Extract IP from URL (remove https://)
    previous_ip = None
    if previous_url:
        previous_ip = previous_url.replace("https://", "").replace("http://", "")

    # FMC IP Address
    while True:
        prompt = f"Enter FMC IP Address"
        if previous_ip:
            prompt += f" [{previous_ip}]"
        else:
            prompt += " (e.g., 192.168.3.122)"
        prompt += ": "

        fmc_ip = input(prompt).strip()

        # Use previous IP if user just presses Enter
        if not fmc_ip and previous_ip:
            fmc_ip = previous_ip

        if fmc_ip:
            # Automatically prepend https://
            fmc_base_url = f"https://{fmc_ip}"
            break
        print("FMC IP Address cannot be empty.")

    # FMC Username
    while True:
        prompt = f"Enter FMC Username"
        if previous_username:
            prompt += f" [{previous_username}]"
        prompt += ": "

        fmc_username = input(prompt).strip()

        # Use previous username if user just presses Enter
        if not fmc_username and previous_username:
            fmc_username = previous_username

        if fmc_username:
            break
        print("FMC Username cannot be empty.")

    # FMC Password (hidden input)
    while True:
        if has_stored_password:
            password_prompt = "Enter FMC Password (or press Enter to use stored password): "
        else:
            password_prompt = "Enter FMC Password: "

        fmc_password = _get_password(password_prompt)

        # If user pressed Enter and we have a stored password, retrieve it
        if not fmc_password and has_stored_password:
            creds = CredentialManager.get_credentials()
            if creds:
                fmc_password = creds.get("fmc_password")

        if fmc_password:
            break

        if has_stored_password:
            print("Please enter a password or press Enter to use the stored one.")
        else:
            print("FMC Password cannot be empty.")

    # Dynamic Object Name
    while True:
        prompt = f"Enter Dynamic Object Name"
        if previous_dynamic:
            prompt += f" [{previous_dynamic}]"
        else:
            prompt += " (e.g., BunnyCDN_Dynamic)"
        prompt += ": "

        fmc_dynamic_name = input(prompt).strip()

        # Use previous dynamic name if user just presses Enter
        if not fmc_dynamic_name and previous_dynamic:
            fmc_dynamic_name = previous_dynamic

        if fmc_dynamic_name:
            break
        print("Dynamic Object Name cannot be empty.")

    # Include IPv6
    default_ipv6_str = "y" if previous_ipv6 else "n"
    include_ipv6_str = input(
        f"Include IPv6 endpoints? (y/n, default: {default_ipv6_str}): "
    ).strip().lower()

    if not include_ipv6_str:
        include_ipv6 = previous_ipv6
    else:
        include_ipv6 = include_ipv6_str in ("y", "yes", "1", "true")

    # Sync interval in minutes
    while True:
        prompt = f"Sync interval in minutes"
        if previous_interval:
            prompt += f" [{previous_interval}]"
        else:
            prompt += " (e.g., 15)"
        prompt += ": "

        interval_str = input(prompt).strip()

        # Use previous interval if user just presses Enter
        if not interval_str and previous_interval:
            interval_str = str(previous_interval)

        try:
            sync_interval_minutes = int(interval_str)
            if sync_interval_minutes > 0:
                break
            print("Interval must be a positive number.")
        except ValueError:
            print("Interval must be a valid integer.")

    # Confirm
    print("\n" + "-" * 60)
    print("Configuration Summary:")
    print(f"  FMC URL:              {fmc_base_url}")
    print(f"  FMC Username:         {fmc_username}")
    print(f"  Dynamic Object Name:  {fmc_dynamic_name}")
    print(f"  Include IPv6:         {include_ipv6}")
    print(f"  Sync Interval:        {sync_interval_minutes} minute(s)")
    print("-" * 60)

    confirm = input("\nSave this configuration? (y/n): ").strip().lower()
    if confirm not in ("y", "yes"):
        print("Setup cancelled.")
        return None

    return {
        "fmc_base_url": fmc_base_url,
        "fmc_username": fmc_username,
        "fmc_password": fmc_password,
        "fmc_dynamic_name": fmc_dynamic_name,
        "include_ipv6": include_ipv6,
        "sync_interval_minutes": sync_interval_minutes,
    }




def setup_cron_job(interval_minutes):
    """
    Set up a cron job to run bunny2fmc at the specified interval.
    Returns True if successful, False otherwise.
    """
    # Find bunny2fmc executable path
    bunny2fmc_path = shutil.which("bunny2fmc")
    if not bunny2fmc_path:
        print("Warning: Could not find bunny2fmc in PATH. Cron job not created.")
        return False
    
    # Create the cron entry
    cron_comment = "# bunny2fmc automatic sync"
    cron_job = f"*/{interval_minutes} * * * * {bunny2fmc_path} --run"
    
    try:
        # Get current crontab
        result = subprocess.run(
            ["crontab", "-l"],
            capture_output=True,
            text=True
        )
        
        if result.returncode == 0:
            current_crontab = result.stdout
        else:
            # No existing crontab
            current_crontab = ""
        
        # Remove any existing bunny2fmc entries
        lines = current_crontab.strip().split("\n") if current_crontab.strip() else []
        new_lines = [line for line in lines if "bunny2fmc" not in line]
        
        # Add new cron job
        new_lines.append(cron_comment)
        new_lines.append(cron_job)
        
        new_crontab = "\n".join(new_lines) + "\n"
        
        # Install new crontab
        process = subprocess.run(
            ["crontab", "-"],
            input=new_crontab,
            capture_output=True,
            text=True
        )
        
        if process.returncode == 0:
            return True
        else:
            print(f"Warning: Failed to install cron job: {process.stderr}")
            return False
            
    except FileNotFoundError:
        print("Warning: crontab command not found. Please set up scheduling manually.")
        return False
    except Exception as e:
        print(f"Warning: Could not set up cron job: {e}")
        return False


def remove_cron_job():
    """Remove bunny2fmc cron job if it exists."""
    try:
        result = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
        if result.returncode != 0:
            return True  # No crontab exists
        
        lines = result.stdout.strip().split("\n")
        new_lines = [line for line in lines if "bunny2fmc" not in line]
        
        if new_lines:
            new_crontab = "\n".join(new_lines) + "\n"
            subprocess.run(["crontab", "-"], input=new_crontab, capture_output=True, text=True)
        else:
            subprocess.run(["crontab", "-r"], capture_output=True, text=True)
        
        return True
    except Exception:
        return False

def cmd_setup(args):
    """Handle --setup flag"""
    config = interactive_setup()
    if not config:
        sys.exit(1)

    try:
        # Store credentials in keyring
        CredentialManager.set_credentials(
            config["fmc_base_url"],
            config["fmc_username"],
            config["fmc_password"],
            config["sync_interval_minutes"],
        )

        # Store config values in config.json (URL, username, dynamic name, IPv6, interval)
        ConfigManager.save_fmc_base_url(config["fmc_base_url"])
        ConfigManager.save_fmc_username(config["fmc_username"])
        ConfigManager.save_dynamic_object_name(config["fmc_dynamic_name"])
        ConfigManager.save_include_ipv6(config["include_ipv6"])
        ConfigManager.save_sync_interval_minutes(config["sync_interval_minutes"])

        print("\n✓ Configuration saved securely!")
        
        # Ask about automatic scheduling
        print(f"\nWould you like to enable automatic sync every {config['sync_interval_minutes']} minute(s)?")
        enable_cron = input("Enable scheduled sync? (y/n, default: y): ").strip().lower()
        
        if enable_cron != "n":
            if setup_cron_job(config["sync_interval_minutes"]):
                print(f"✓ Cron job installed: runs every {config['sync_interval_minutes']} minute(s)")
            else:
                print(f"\nTo manually schedule with cron:")
                print(f"  */{config['sync_interval_minutes']} * * * * bunny2fmc --run")
        else:
            print(f"\nTo manually schedule with cron later:")
            print(f"  */{config['sync_interval_minutes']} * * * * bunny2fmc --run")
        
        print(f"\nTo run the sync immediately:")
        print(f"  bunny2fmc --run")
        sys.exit(0)
    except Exception as e:
        print(f"\n✗ Failed to save configuration: {e}")
        sys.exit(1)


def cmd_show_config(args):
    """Handle --config flag: show current configuration"""
    try:
        # Load config
        fmc_url = ConfigManager.load_fmc_base_url()
        fmc_username = ConfigManager.load_fmc_username()
        fmc_dynamic = ConfigManager.load_dynamic_object_name()
        include_ipv6 = ConfigManager.load_include_ipv6()
        sync_interval = ConfigManager.load_sync_interval_minutes()

        if not all([fmc_url, fmc_username, fmc_dynamic, sync_interval is not None]):
            print("No configuration found. Run: bunny2fmc --setup")
            sys.exit(1)

        print("\nCurrent Configuration:")
        print("-" * 60)
        print(f"  FMC URL:              {fmc_url}")
        print(f"  FMC Username:         {fmc_username}")
        print(f"  Dynamic Object Name:  {fmc_dynamic}")
        print(f"  Include IPv6:         {include_ipv6}")
        print(f"  Sync Interval:        {sync_interval} minute(s)")
        print("-" * 60)
        print("\n✓ Credentials are stored securely in your OS keyring.")
        print(f"\nLog file: {ConfigManager.get_log_file()}")
        sys.exit(0)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)


def cmd_clear_config(args):
    """Handle --clear flag: clear all stored configuration"""
    confirm = input(
        "\nWarning: This will clear all stored credentials and configuration.\n"
        "Continue? (y/n): "
    ).strip().lower()
    if confirm not in ("y", "yes"):
        print("Cancelled.")
        sys.exit(0)

    try:
        CredentialManager.clear_credentials()
        config_file = ConfigManager.get_config_dir() / "config.json"
        if config_file.exists():
            config_file.unlink()
        print("✓ Configuration cleared.")
        sys.exit(0)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)


def cmd_run(args, logger):
    """Handle --run flag or normal execution"""
    # Load credentials from keyring
    creds = CredentialManager.get_credentials()
    if not creds:
        logger.error("No stored credentials found. Run: bunny2fmc --setup")
        print("Error: No stored credentials found.\nPlease run: bunny2fmc --setup")
        sys.exit(1)

    # Load additional config
    fmc_dynamic_name = ConfigManager.load_dynamic_object_name()
    include_ipv6 = ConfigManager.load_include_ipv6()

    if not fmc_dynamic_name:
        logger.error("Dynamic Object name not configured. Run: bunny2fmc --setup")
        print(
            "Error: Dynamic Object name not configured.\n"
            "Please run: bunny2fmc --setup"
        )
        sys.exit(1)

    # Run the sync
    try:
        logger.info(
            f"Starting sync (IPv6: {include_ipv6}, Dynamic Object: {fmc_dynamic_name})"
        )
        sync(
            fmc_base_url=creds["fmc_base_url"],
            fmc_username=creds["fmc_username"],
            fmc_password=creds["fmc_password"],
            fmc_dynamic_name=fmc_dynamic_name,
            include_ipv6=include_ipv6,
        )
        logger.info("Sync completed successfully")
        print("✓ Sync completed successfully")
        sys.exit(0)
    except Exception as e:
        logger.error(f"Sync failed: {e}")
        print(f"Error: {e}")
        sys.exit(1)


def cmd_logs(args):
    """Handle --logs flag - view or follow log file"""
    import subprocess
    
    log_file = ConfigManager.get_log_file()
    
    if not log_file.exists():
        print(f"Error: Log file not found at {log_file}")
        print("No syncs have been run yet")
        sys.exit(1)
    
    # Check if follow mode requested
    follow = getattr(args, 'logs_follow', False)
    
    if follow:
        # Follow log in realtime
        try:
            subprocess.run(['tail', '-f', str(log_file)])
        except KeyboardInterrupt:
            print("\n✓ Log following stopped")
            sys.exit(0)
    else:
        # Show last 20 lines
        try:
            result = subprocess.run(['tail', '-20', str(log_file)], capture_output=True, text=True)
            print(result.stdout)
            sys.exit(0)
        except Exception as e:
            print(f"Error reading log file: {e}")
            sys.exit(1)


def cmd_start(args):
    """Handle --start flag - re-enables scheduled syncs with existing config"""
    try:
        # Check if config exists
        fmc_base_url = ConfigManager.load_fmc_base_url()
        fmc_username = ConfigManager.load_fmc_username()
        fmc_dynamic_name = ConfigManager.load_dynamic_object_name()
        
        if not all([fmc_base_url, fmc_username, fmc_dynamic_name]):
            print("Error: No configuration found. Please run: bunny2fmc --setup")
            sys.exit(1)
        
        # Display current config
        print("\nCurrent configuration:")
        print(f"  FMC URL: {fmc_base_url}")
        print(f"  FMC Username: {fmc_username}")
        print(f"  Dynamic Object: {fmc_dynamic_name}")
        
        # Ask for sync interval
        print("\nEnter sync interval (in minutes):")
        while True:
            interval_input = input("  Enter interval [1-1440]: ").strip()
            try:
                interval = int(interval_input)
                if 1 <= interval <= 1440:
                    break
                print("  Invalid: Please enter a value between 1 and 1440")
            except ValueError:
                print("  Invalid: Please enter a number")
        
        # Save interval and setup cron
        ConfigManager.save_sync_interval_minutes(interval)
        setup_cron_job(interval)
        
        print(f"\n✓ Scheduled syncs re-enabled")
        print(f"✓ Cron job configured for every {interval} minute(s)")
        print(f"✓ Next sync will run in {interval} minute(s)")
        sys.exit(0)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)


def cmd_init(args):
    """Copy documentation files to current directory"""
    from pathlib import Path
    
    # Find the package installation directory
    package_dir = Path(__file__).parent.parent
    
    doc_files = ['QUICK_START.md', 'LINUX_INSTALL.md', 'README.md', 'INSTALL.md', 'install.sh']
    copied = []
    skipped = []
    
    for doc_file in doc_files:
        src = package_dir / doc_file
        dst = Path.cwd() / doc_file
        
        if not src.exists():
            continue
        
        if dst.exists():
            skipped.append(doc_file)
            continue
        
        try:
            shutil.copy2(src, dst)
            copied.append(doc_file)
        except Exception as e:
            print(f"Warning: Could not copy {doc_file}: {e}")
    
    if copied:
        print("✓ Documentation files copied to current directory:")
        for f in copied:
            print(f"  - {f}")
    
    if skipped:
        print("\nSkipped (already exist):")
        for f in skipped:
            print(f"  - {f}")
    
    if not copied and not skipped:
        print("No documentation files found in package.")
        sys.exit(1)
    
    print("\n→ Start with: cat QUICK_START.md")
    sys.exit(0)


def cmd_stop(args):
    """Handle --stop flag - removes cron job and stops scheduled syncs"""
    if remove_cron_job():
        print("✓ Cron job removed successfully")
        print("Scheduled syncs have been stopped")
        sys.exit(0)
    else:
        print("Error: Failed to remove cron job")
        sys.exit(1)


def main():
    """Main entry point with argparse"""
    parser = argparse.ArgumentParser(
        prog="bunny2fmc",
        description="bunny2fmc - Sync BunnyCDN IP ranges to Cisco FMC Dynamic Objects",
        epilog=_EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument(
        "--version",
        action="version",
        version=f"%(prog)s {bunny2fmc.__version__}",
        help="Show version and exit",
    )

    parser.add_argument(
        "--setup",
        action="store_true",
        help="Run interactive setup wizard",
    )

    parser.add_argument(
        "--config",
        action="store_true",
        help="Show current configuration",
    )

    parser.add_argument(
        "--clear",
        action="store_true",
        help="Clear all stored configuration and credentials",
    )

    parser.add_argument(
        "--run",
        action="store_true",
        help="Run sync immediately (can also just run 'bunny2fmc')",
    )

    parser.add_argument(
        "--logs",
        nargs='?',
        const='view',
        metavar='MODE',
        help="View or follow log file (MODE: 'view' or 'follow', default: view)",
    )

    parser.add_argument(
        "--start",
        action="store_true",
        help="Start scheduled syncs (re-enable with existing config)",
    )

    parser.add_argument(
        "--stop",
        action="store_true",
        help="Stop scheduled syncs (remove cron job)",
    )

    parser.add_argument(
        "--init",
        action="store_true",
        help="Copy documentation files (QUICK_START.md, etc.) to current directory",
    )

    args = parser.parse_args()

    # Setup logging
    logger = setup_logging()
    logger.info(f"bunny2fmc started with args: {sys.argv[1:]}")

    # Handle commands
    if args.setup:
        cmd_setup(args)
    elif args.config:
        cmd_show_config(args)
    elif args.clear:
        cmd_clear_config(args)
    elif args.logs is not None:
        # Parse logs mode: 'view' (default) or 'follow'
        if args.logs == 'follow':
            args.logs_follow = True
        else:
            args.logs_follow = False
        cmd_logs(args)
    elif args.start:
        cmd_start(args)
    elif args.stop:
        cmd_stop(args)
    elif args.init:
        cmd_init(args)
    else:
        # Default: run sync
        cmd_run(args, logger)


if __name__ == "__main__":
    main()
