#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
OSPF Monitor Script
Проверяет статус, роли и детальную информацию OSPF на маршрутизаторах Cisco/Huawei.
Сравнивает с предыдущим состоянием, выводит цветную таблицу в консоль.
https://chat.deepseek.com/share/b4paob4qj50o36wrih
"""

import json
import os
import re
from datetime import datetime
from typing import Dict, List, Optional, Tuple
import paramiko
from colorama import init, Fore, Style, Back

# Инициализация цветов для Windows/Linux/Mac
init(autoreset=True)

# ==================== КОНФИГУРАЦИЯ ====================
LOGIN = "root"
PASSWORD = "your_password_here"
OSPF_ROUTERS = [
    "192.168.1.1",
    "192.168.1.2",
    "192.168.1.3",
    "192.168.1.4"
]
STATE_FILE = "ospf_state.json"      # Файл для хранения предыдущего состояния
SSH_PORT = 22
SSH_TIMEOUT = 10                    # Таймаут подключения в секундах

# ==================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================

def ssh_exec_command(ip: str, command: str) -> Tuple[bool, str]:
    """
    Выполняет команду через SSH и возвращает (успех, вывод_или_ошибка)
    Автоматически определяет тип устройства по prompt
    """
    client = paramiko.SSHClient()
    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    try:
        client.connect(
            hostname=ip,
            port=SSH_PORT,
            username=LOGIN,
            password=PASSWORD,
            timeout=SSH_TIMEOUT,
            look_for_keys=False,
            allow_agent=False
        )
        
        # Отправляем команду и ждем результат
        # Для Huawei и Cisco команды одинаковы в данном контексте
        stdin, stdout, stderr = client.exec_command(command, timeout=SSH_TIMEOUT)
        output = stdout.read().decode('utf-8', errors='ignore')
        error = stderr.read().decode('utf-8', errors='ignore')
        
        client.close()
        
        if error and "Error" in error:
            return False, f"CLI Error: {error.strip()}"
        
        return True, output.strip()
        
    except paramiko.AuthenticationException:
        return False, "Authentication failed"
    except paramiko.SSHException as e:
        return False, f"SSH error: {str(e)}"
    except Exception as e:
        return False, f"Connection error: {str(e)}"

def detect_device_type(ip: str) -> Optional[str]:
    """
    Определяет тип устройства по версии ОС или prompt
    """
    commands = [
        "display version",      # Huawei
        "show version"          # Cisco
    ]
    
    for cmd in commands:
        success, output = ssh_exec_command(ip, cmd)
        if success:
            output_lower = output.lower()
            if "huawei" in output_lower or "versatile routing platform" in output_lower:
                return "huawei"
            elif "cisco" in output_lower or "ios" in output_lower:
                return "cisco"
    
    return None

def get_ospf_info_cisco(ip: str) -> Dict:
    """
    Собирает максимум информации по OSPF на Cisco
    """
    info = {
        "router_id": "N/A",
        "ospf_enabled": False,
        "processes": [],
        "role": "UNKNOWN",
        "areas": [],
        "neighbors": 0,
        "neighbors_full": 0,
        "interfaces": [],
        "lsa_count": 0,
        "spf_runs": 0,
        "last_spf": "N/A"
    }
    
    # Проверка, включен ли OSPF
    success, output = ssh_exec_command(ip, "show ip ospf")
    if not success or "Routing Process" not in output:
        info["ospf_enabled"] = False
        info["role"] = "OSPF_DISABLED"
        return info
    
    info["ospf_enabled"] = True
    
    # Парсим Router ID
    match = re.search(r"Router ID (\d+\.\d+\.\d+\.\d+)", output)
    if match:
        info["router_id"] = match.group(1)
    
    # Парсим SPF статистику
    match = re.search(r"Number of SPF runs (\d+)", output)
    if match:
        info["spf_runs"] = int(match.group(1))
    
    match = re.search(r"Last SPF executed ([\d\w\s:]+)", output)
    if match:
        info["last_spf"] = match.group(1).strip()
    
    # Получаем информацию об интерфейсах
    success, output = ssh_exec_command(ip, "show ip ospf interface brief")
    if success:
        lines = output.splitlines()
        for line in lines[1:]:  # Пропускаем заголовок
            parts = line.split()
            if len(parts) >= 5:
                interface = parts[0]
                area = parts[2] if len(parts) > 2 else "N/A"
                role = "UNKNOWN"
                if "DR" in line:
                    role = "DR"
                elif "BDR" in line:
                    role = "BDR"
                else:
                    role = "DROTHER"
                
                info["interfaces"].append({
                    "name": interface,
                    "area": area,
                    "role": role
                })
                
                # Глобальная роль - берем самую старшую (DR > BDR > DROTHER)
                if role == "DR" and info["role"] not in ["DR"]:
                    info["role"] = "DR"
                elif role == "BDR" and info["role"] not in ["DR", "BDR"]:
                    info["role"] = "BDR"
                elif info["role"] == "UNKNOWN":
                    info["role"] = role
    
    # Получаем соседей
    success, output = ssh_exec_command(ip, "show ip ospf neighbor")
    if success:
        lines = output.splitlines()
        for line in lines:
            if "FULL" in line:
                info["neighbors_full"] += 1
                info["neighbors"] += 1
            elif "EXSTART" in line or "EXCHANGE" in line or "LOADING" in line:
                info["neighbors"] += 1
    
    # Получаем LSA информацию
    success, output = ssh_exec_command(ip, "show ip ospf database summary")
    if success:
        match = re.search(r"Total\s+LSA\s+count:\s+(\d+)", output, re.IGNORECASE)
        if match:
            info["lsa_count"] = int(match.group(1))
    
    # Получаем информацию об Area
    success, output = ssh_exec_command(ip, "show ip ospf border-routers")
    if success:
        if "Area" in output:
            areas = re.findall(r"Area (\d+\.\d+\.\d+\.\d+)", output)
            info["areas"] = list(set(areas)) if areas else ["0.0.0.0"]
    else:
        info["areas"] = ["0.0.0.0"]  # По умолчанию backbone
    
    return info

def get_ospf_info_huawei(ip: str) -> Dict:
    """
    Собирает максимум информации по OSPF на Huawei
    """
    info = {
        "router_id": "N/A",
        "ospf_enabled": False,
        "processes": [],
        "role": "UNKNOWN",
        "areas": [],
        "neighbors": 0,
        "neighbors_full": 0,
        "interfaces": [],
        "lsa_count": 0,
        "spf_runs": 0,
        "last_spf": "N/A"
    }
    
    # Проверка OSPF
    success, output = ssh_exec_command(ip, "display ospf")
    if not success or "OSPF Process" not in output:
        info["ospf_enabled"] = False
        info["role"] = "OSPF_DISABLED"
        return info
    
    info["ospf_enabled"] = True
    
    # Router ID
    match = re.search(r"Router ID\s+:\s+(\d+\.\d+\.\d+\.\d+)", output)
    if match:
        info["router_id"] = match.group(1)
    
    # SPF runs
    match = re.search(r"SPF\s+schedule\s+count\s+:\s+(\d+)", output)
    if match:
        info["spf_runs"] = int(match.group(1))
    
    # Интерфейсы и роли
    success, output = ssh_exec_command(ip, "display ospf interface")
    if success:
        lines = output.splitlines()
        current_interface = None
        for line in lines:
            if "Interface:" in line and "(" in line:
                current_interface = line.split()[1].split('(')[0]
            if current_interface and "Area" in line:
                area_match = re.search(r"Area\s+(\d+\.\d+\.\d+\.\d+)", line)
                area = area_match.group(1) if area_match else "N/A"
                role = "UNKNOWN"
                if "DR" in output[lines.index(line):lines.index(line)+10]:
                    role = "DR"
                elif "BDR" in output[lines.index(line):lines.index(line)+10]:
                    role = "BDR"
                else:
                    role = "DROTHER"
                
                info["interfaces"].append({
                    "name": current_interface,
                    "area": area,
                    "role": role
                })
                
                if role == "DR" and info["role"] not in ["DR"]:
                    info["role"] = "DR"
                elif role == "BDR" and info["role"] not in ["DR", "BDR"]:
                    info["role"] = "BDR"
                elif info["role"] == "UNKNOWN":
                    info["role"] = role
    
    # Соседи
    success, output = ssh_exec_command(ip, "display ospf peer brief")
    if success:
        lines = output.splitlines()
        for line in lines:
            if "Full" in line:
                info["neighbors_full"] += 1
                info["neighbors"] += 1
            elif "2-Way" not in line and "Down" not in line and "Peer ID" not in line:
                if line.strip() and not line.startswith("-"):
                    info["neighbors"] += 1
    
    # LSA count
    success, output = ssh_exec_command(ip, "display ospf lsdb brief")
    if success:
        lsa_lines = [l for l in output.splitlines() if "LSA" in l and "Type" not in l]
        info["lsa_count"] = len(lsa_lines)
    
    # Area information
    success, output = ssh_exec_command(ip, "display ospf brief")
    if success:
        areas = re.findall(r"Area\s+(\d+\.\d+\.\d+\.\d+)", output)
        info["areas"] = list(set(areas)) if areas else ["0.0.0.0"]
    else:
        info["areas"] = ["0.0.0.0"]
    
    return info

def get_full_ospf_info(ip: str) -> Tuple[bool, Dict]:
    """
    Получает полную информацию по OSPF с автоопределением типа устройства
    """
    # Сначала проверяем доступность
    success, output = ssh_exec_command(ip, "echo test")
    if not success:
        return False, {"error": "UNREACHABLE", "ospf_enabled": False}
    
    # Определяем тип устройства
    device_type = detect_device_type(ip)
    
    if device_type == "cisco":
        return True, get_ospf_info_cisco(ip)
    elif device_type == "huawei":
        return True, get_ospf_info_huawei(ip)
    else:
        # Пробуем оба варианта
        cisco_info = get_ospf_info_cisco(ip)
        if cisco_info["ospf_enabled"]:
            return True, cisco_info
        huawei_info = get_ospf_info_huawei(ip)
        if huawei_info["ospf_enabled"]:
            return True, huawei_info
        return False, {"error": "UNKNOWN_DEVICE", "ospf_enabled": False}

def load_previous_state() -> Dict:
    """Загружает предыдущее состояние из JSON файла"""
    if not os.path.exists(STATE_FILE):
        return {}
    
    try:
        with open(STATE_FILE, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (json.JSONDecodeError, IOError):
        print(f"{Fore.YELLOW}Warning: Could not load state file, starting fresh{Style.RESET_ALL}")
        return {}

def save_current_state(state: Dict):
    """Сохраняет текущее состояние в JSON файл"""
    with open(STATE_FILE, 'w', encoding='utf-8') as f:
        json.dump(state, f, indent=2, ensure_ascii=False)

def print_table(current_data: Dict[str, Dict], previous_data: Dict[str, Dict]):
    """
    Выводит подробную таблицу с цветовой индикацией изменений
    """
    # Заголовок
    print(f"\n{Back.BLUE}{Fore.WHITE}{'='*140}{Style.RESET_ALL}")
    print(f"{Back.BLUE}{Fore.WHITE}OSPF MONITOR REPORT - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}{Style.RESET_ALL}")
    print(f"{Back.BLUE}{Fore.WHITE}{'='*140}{Style.RESET_ALL}\n")
    
    # Шапка таблицы
    headers = [
        "IP Address", "Status", "Router ID", "Role", "Neighbors",
        "Full/Nbrs", "LSA Count", "SPF Runs", "Areas", "Interface Role"
    ]
    
    # Определяем ширину колонок (примерная)
    col_widths = [16, 10, 16, 10, 10, 12, 11, 10, 20, 15]
    
    # Выводим заголовок
    header_line = ""
    for i, header in enumerate(headers):
        header_line += f"{Fore.CYAN}{header:<{col_widths[i]}}{Style.RESET_ALL} "
    print(header_line)
    print(f"{Fore.CYAN}{'-'*140}{Style.RESET_ALL}")
    
    # Выводим данные по каждому маршрутизатору
    for ip in sorted(current_data.keys()):
        current = current_data[ip]
        previous = previous_data.get(ip, {})
        
        # Статус устройства
        if "error" in current:
            status = f"{Fore.RED}UNREACHABLE{Style.RESET_ALL}"
        elif not current.get("ospf_enabled", False):
            status = f"{Fore.RED}OSPF OFF{Style.RESET_ALL}"
        elif current.get("role") == "UNKNOWN":
            status = f"{Fore.YELLOW}NO ACTIVE{Style.RESET_ALL}"
        else:
            status = f"{Fore.GREEN}OK{Style.RESET_ALL}"
        
        # Router ID (с подсветкой изменений)
        router_id = current.get("router_id", "N/A")
        prev_router_id = previous.get("router_id", "")
        if router_id != prev_router_id and prev_router_id:
            router_id = f"{Fore.RED}{router_id}{Style.RESET_ALL}"
        
        # Role (главная роль маршрутизатора)
        role = current.get("role", "UNKNOWN")
        prev_role = previous.get("role", "")
        if role != prev_role and prev_role:
            role_colored = f"{Fore.RED}{role}{Style.RESET_ALL}"
        else:
            role_colored = f"{Fore.GREEN}{role}{Style.RESET_ALL}" if role != "UNKNOWN" else f"{Fore.YELLOW}{role}{Style.RESET_ALL}"
        
        # Neighbors
        neighbors = current.get("neighbors", 0)
        neighbors_full = current.get("neighbors_full", 0)
        neighbors_info = f"{neighbors_full}/{neighbors}" if neighbors_full > 0 else f"0/{neighbors}"
        prev_neighbors = previous.get("neighbors_full", 0)
        if neighbors_full != prev_neighbors and prev_neighbors:
            neighbors_info = f"{Fore.RED}{neighbors_info}{Style.RESET_ALL}"
        
        # LSA count
        lsa_count = current.get("lsa_count", 0)
        prev_lsa = previous.get("lsa_count", 0)
        if lsa_count != prev_lsa and prev_lsa:
            lsa_colored = f"{Fore.RED}{lsa_count}{Style.RESET_ALL}"
        else:
            lsa_colored = str(lsa_count)
        
        # SPF runs
        spf_runs = current.get("spf_runs", 0)
        prev_spf = previous.get("spf_runs", 0)
        if spf_runs != prev_spf and prev_spf:
            spf_colored = f"{Fore.RED}{spf_runs}{Style.RESET_ALL}"
        else:
            spf_colored = str(spf_runs)
        
        # Areas
        areas = ", ".join(current.get("areas", ["N/A"]))
        if len(areas) > 20:
            areas = areas[:17] + "..."
        prev_areas = ", ".join(previous.get("areas", []))
        if areas != prev_areas and prev_areas:
            areas = f"{Fore.RED}{areas}{Style.RESET_ALL}"
        
        # Interface role (детализация)
        interface_role = "N/A"
        if current.get("interfaces") and len(current["interfaces"]) > 0:
            roles = [iface["role"] for iface in current["interfaces"] if iface["role"] != "UNKNOWN"]
            if roles:
                interface_role = ", ".join(set(roles))
        
        # Собираем строку
        row_data = [
            ip, status, router_id, role_colored, neighbors_info,
            f"{neighbors_full}/{neighbors}", lsa_colored, spf_colored, areas, interface_role
        ]
        
        row_line = ""
        for i, data in enumerate(row_data):
            # Убираем цветовые коды для правильного выравнивания (простое решение)
            clean_data = re.sub(r'\x1b\[[0-9;]*m', '', str(data))
            padding = col_widths[i] - len(clean_data)
            if padding < 0:
                padding = 0
            row_line += f"{data}{' ' * padding} "
        
        print(row_line)
        
        # Если есть детализация по интерфейсам, показываем её дополнительно
        if current.get("interfaces") and not current.get("error"):
            for iface in current["interfaces"][:3]:  # Показываем не более 3 интерфейсов
                iface_role = iface["role"]
                prev_iface_roles = [i["role"] for i in previous.get("interfaces", []) if i["name"] == iface["name"]]
                if prev_iface_roles and iface_role != prev_iface_roles[0]:
                    iface_role = f"{Fore.RED}{iface_role}{Style.RESET_ALL}"
                print(f"  {Fore.CYAN}L-{Style.RESET_ALL} Interface: {iface['name']:<12} Area: {iface['area']:<12} Role: {iface_role}")
    
    print(f"\n{Fore.CYAN}{'='*140}{Style.RESET_ALL}")
    print(f"{Fore.GREEN}? Green: No change  |  {Fore.RED}? Red: Value changed since last run  |  {Fore.YELLOW}? Warning{Style.RESET_ALL}")

def main():
    """Основная функция"""
    print(f"{Fore.YELLOW}Starting OSPF Monitor...{Style.RESET_ALL}")
    print(f"Devices to check: {len(OSPF_ROUTERS)}")
    
    # Загружаем предыдущее состояние
    previous_state = load_previous_state()
    
    # Собираем текущее состояние
    current_state = {}
    
    for ip in OSPF_ROUTERS:
        print(f"  Checking {ip}...", end=" ", flush=True)
        success, info = get_full_ospf_info(ip)
        if success:
            current_state[ip] = info
            if info.get("ospf_enabled", False):
                print(f"{Fore.GREEN}OK (Role: {info.get('role', 'UNKNOWN')}){Style.RESET_ALL}")
            else:
                print(f"{Fore.YELLOW}OSPF Disabled{Style.RESET_ALL}")
        else:
            current_state[ip] = {"error": info.get("error", "UNKNOWN"), "ospf_enabled": False}
            print(f"{Fore.RED}FAILED - {info.get('error', 'Unknown error')}{Style.RESET_ALL}")
    
    # Выводим таблицу
    print_table(current_state, previous_state)
    
    # Сохраняем новое состояние
    save_current_state(current_state)
    print(f"\n{Fore.GREEN}State saved to {STATE_FILE}{Style.RESET_ALL}")

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print(f"\n{Fore.YELLOW}Interrupted by user{Style.RESET_ALL}")
    except Exception as e:
        print(f"{Fore.RED}Fatal error: {e}{Style.RESET_ALL}")