"""
CMDB 자동 발견 오케스트레이터 — 에이전트리스

네트워크 범위 스캔 → SSH 인벤토리 수집 → CMDB 자동 등록.
대상 서버에 소프트웨어 설치 불필요 (SSH/SNMP만 사용).

엔드포인트:
  POST /api/autodiscovery/scan          — 네트워크 범위 스캔 시작
  GET  /api/autodiscovery/jobs          — 스캔 작업 목록
  GET  /api/autodiscovery/jobs/{id}     — 작업 상태·결과
  POST /api/autodiscovery/apply/{id}    — 발견 결과 → CMDB 적용
  GET  /api/autodiscovery/schedule      — 자동 스캔 스케줄
  POST /api/autodiscovery/schedule      — 스케줄 등록
  DELETE /api/autodiscovery/schedule/{id} — 스케줄 삭제
"""
from __future__ import annotations

import asyncio
import ipaddress
import json
import logging
from datetime import datetime
from typing import Optional

import paramiko
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession

from core.auth import get_current_user, require_admin_role
from database import get_db
from models import User, Server, CMDBAutoDiscovery, DiscoveryScanJob

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/autodiscovery", tags=["CMDB 자동 발견"])


class ScanRequest(BaseModel):
    network_range: str = Field(..., description="CIDR 표기 (예: 192.168.1.0/24)")
    ssh_user: str = Field("root", description="SSH 계정")
    ssh_password: Optional[str] = None
    ssh_key_path: Optional[str] = None
    include_snmp: bool = False
    snmp_community: str = "public"
    auto_register: bool = False  # 발견 즉시 CMDB 등록


async def _probe_host(ip: str, ssh_user: str, ssh_password: Optional[str]) -> Optional[dict]:
    """단일 호스트 SSH 프로브 — 에이전트리스."""
    try:
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(ip, username=ssh_user, password=ssh_password,
                    timeout=5, banner_timeout=10)

        results = {}
        commands = {
            "hostname":    "hostname -f 2>/dev/null || hostname",
            "os":          "cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'\"' -f2 || uname -s",
            "cpu_cores":   "nproc 2>/dev/null || echo 0",
            "cpu_model":   "grep 'model name' /proc/cpuinfo 2>/dev/null | head -1 | cut -d: -f2 | xargs || echo unknown",
            "memory_mb":   "free -m 2>/dev/null | awk '/^Mem:/{print $2}' || echo 0",
            "disk_gb":     "df -BG / 2>/dev/null | awk 'NR==2{gsub(/G/,\"\",$2); print $2}' || echo 0",
            "open_ports":  "ss -tlnp 2>/dev/null | awk 'NR>1{print $4}' | grep -oE '[0-9]+$' | sort -n | tr '\n' ',' || echo ''",
            "services":    "systemctl list-units --type=service --state=running 2>/dev/null | grep '.service' | awk '{print $1}' | head -10 | tr '\n' ',' || echo ''",
            "java_version":"java -version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+' | head -1 || echo none",
            "python_ver":  "python3 --version 2>/dev/null | cut -d' ' -f2 || echo none",
            "pkg_count":   "dpkg -l 2>/dev/null | grep '^ii' | wc -l || rpm -qa 2>/dev/null | wc -l || echo 0",
        }

        for key, cmd in commands.items():
            try:
                _, stdout, _ = ssh.exec_command(cmd, timeout=5)
                results[key] = stdout.read().decode('utf-8', 'replace').strip()
            except Exception:
                results[key] = ""

        ssh.close()
        results["ip"] = ip
        results["status"] = "REACHABLE"
        return results
    except paramiko.AuthenticationException:
        return {"ip": ip, "status": "AUTH_FAILED"}
    except Exception:
        return None  # 응답 없음 → 스킵


async def _run_scan(job_id: int, request: ScanRequest, db: AsyncSession):
    """백그라운드 스캔 실행."""
    job_row = await db.execute(select(DiscoveryScanJob).where(DiscoveryScanJob.id == job_id))
    job = job_row.scalar_one_or_none()
    if not job:
        return

    try:
        job.status = "RUNNING"
        await db.commit()

        network = ipaddress.ip_network(request.network_range, strict=False)
        hosts = list(network.hosts())
        # 최대 254개 호스트로 제한
        hosts = hosts[:254]

        discovered = []
        for host in hosts:
            ip = str(host)
            result = await _probe_host(ip, request.ssh_user, request.ssh_password)
            if result and result.get("status") == "REACHABLE":
                discovered.append(result)
                # 발견 즉시 CMDB 등록 (auto_register=True인 경우)
                if request.auto_register:
                    existing = await db.execute(
                        select(Server).where(Server.ip_addr == ip)
                    )
                    if not existing.scalar_one_or_none():
                        new_server = Server(
                            ip_addr=ip,
                            hostname=result.get("hostname", ip),
                            os_type=result.get("os", "unknown"),
                            ssh_user=request.ssh_user,
                            cpu_cores=int(result.get("cpu_cores", 0) or 0),
                            memory_mb=int(result.get("memory_mb", 0) or 0),
                            discovered_at=datetime.utcnow(),
                        )
                        db.add(new_server)

            # 각 호스트 결과를 CMDBAutoDiscovery에 기록
            discovery = CMDBAutoDiscovery(
                job_id=job_id,
                ip_addr=ip,
                status=result.get("status", "UNREACHABLE") if result else "UNREACHABLE",
                discovered_data=result or {},
                discovered_at=datetime.utcnow(),
            )
            db.add(discovery)

        await db.commit()
        job.status = "DONE"
        job.discovered_count = len(discovered)
        job.result_summary = json.dumps(
            {"total_scanned": len(hosts), "discovered": len(discovered)},
            ensure_ascii=False
        )
    except Exception as e:
        job.status = "FAILED"
        job.error_message = str(e)[:500]
        logger.error(f"스캔 실패 (job {job_id}): {e}")
    finally:
        job.finished_at = datetime.utcnow()
        await db.commit()


@router.post("/scan")
async def start_scan(
    req: ScanRequest,
    background_tasks: BackgroundTasks,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(require_admin_role),
):
    """네트워크 범위 스캔 시작."""
    # CIDR 유효성 검사
    try:
        network = ipaddress.ip_network(req.network_range, strict=False)
        host_count = sum(1 for _ in network.hosts())
    except ValueError:
        raise HTTPException(400, f"유효하지 않은 CIDR: {req.network_range}")

    if host_count > 1024:
        raise HTTPException(400, "한 번에 최대 1024개 호스트까지 스캔 가능합니다")

    job = DiscoveryScanJob(
        tenant_id=user.tenant_id,
        network_range=req.network_range,
        ssh_user=req.ssh_user,
        include_snmp=req.include_snmp,
        auto_register=req.auto_register,
        status="QUEUED",
        host_count=host_count,
        created_by=user.id,
        created_at=datetime.utcnow(),
    )
    db.add(job)
    await db.commit()
    await db.refresh(job)

    background_tasks.add_task(_run_scan, job.id, req, db)
    return {"ok": True, "job_id": job.id, "host_count": host_count}


@router.get("/jobs")
async def list_jobs(
    limit: int = 20,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_current_user),
):
    rows = await db.execute(
        select(DiscoveryScanJob).where(DiscoveryScanJob.tenant_id == user.tenant_id)
        .order_by(desc(DiscoveryScanJob.created_at)).limit(limit)
    )
    jobs = rows.scalars().all()
    return [
        {
            "id": j.id, "network": j.network_range, "status": j.status,
            "host_count": j.host_count, "discovered": j.discovered_count or 0,
            "created_at": j.created_at, "finished_at": j.finished_at,
        }
        for j in jobs
    ]


@router.get("/jobs/{job_id}")
async def get_job(
    job_id: int,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_current_user),
):
    job_row = await db.execute(
        select(DiscoveryScanJob).where(
            DiscoveryScanJob.id == job_id,
            DiscoveryScanJob.tenant_id == user.tenant_id,
        )
    )
    job = job_row.scalar_one_or_none()
    if not job:
        raise HTTPException(404, "스캔 작업 없음")

    results_row = await db.execute(
        select(CMDBAutoDiscovery).where(CMDBAutoDiscovery.job_id == job_id)
        .order_by(CMDBAutoDiscovery.ip_addr)
    )
    results = results_row.scalars().all()
    return {
        "job": {
            "id": job.id, "network": job.network_range, "status": job.status,
            "host_count": job.host_count, "discovered": job.discovered_count or 0,
            "summary": json.loads(job.result_summary or "{}"),
            "error": job.error_message,
        },
        "results": [
            {
                "ip": r.ip_addr, "status": r.status,
                "hostname": (r.discovered_data or {}).get("hostname", ""),
                "os": (r.discovered_data or {}).get("os", ""),
                "cpu": (r.discovered_data or {}).get("cpu_cores", 0),
                "mem_mb": (r.discovered_data or {}).get("memory_mb", 0),
                "open_ports": (r.discovered_data or {}).get("open_ports", ""),
                "in_cmdb": r.in_cmdb,
            }
            for r in results
        ],
    }


@router.post("/apply/{job_id}")
async def apply_to_cmdb(
    job_id: int,
    db: AsyncSession = Depends(get_db),
    user: User = Depends(require_admin_role),
):
    """발견 결과를 CMDB에 등록."""
    results_row = await db.execute(
        select(CMDBAutoDiscovery).where(
            CMDBAutoDiscovery.job_id == job_id,
            CMDBAutoDiscovery.status == "REACHABLE",
            CMDBAutoDiscovery.in_cmdb == False,
        )
    )
    results = results_row.scalars().all()

    registered = 0
    for r in results:
        data = r.discovered_data or {}
        ip = r.ip_addr
        existing = await db.execute(select(Server).where(Server.ip_addr == ip))
        if not existing.scalar_one_or_none():
            new_server = Server(
                ip_addr=ip,
                hostname=data.get("hostname", ip),
                os_type=data.get("os", "unknown"),
                ssh_user="opsagent",
                cpu_cores=int(data.get("cpu_cores", 0) or 0),
                memory_mb=int(data.get("memory_mb", 0) or 0),
                discovered_at=datetime.utcnow(),
            )
            db.add(new_server)
            r.in_cmdb = True
            registered += 1

    await db.commit()
    return {"ok": True, "registered": registered}
