"""
BizNode Daily Maintenance
=========================
Automated daily task that runs at 5:00 PM via Windows Task Scheduler.

What it does (in order):
  1. Check for changes — skip if nothing changed
  2. Sync core files from root → basic/
  3. Rebuild BizNode.zip (basic package only, NEVER pro)
  4. Re-seed knowledge base
  5. Git commit + push to private repo
  6. Deploy website via update.ps1
  7. Restart bot

Usage:
    python scripts/daily_maintenance.py          # Full run
    python scripts/daily_maintenance.py --dry-run # Preview only
    python scripts/daily_maintenance.py --force   # Run even if no changes
    python scripts/daily_maintenance.py --step sync   # Run single step
    python scripts/daily_maintenance.py --step build
    python scripts/daily_maintenance.py --step kb
    python scripts/daily_maintenance.py --step commit
    python scripts/daily_maintenance.py --step deploy
    python scripts/daily_maintenance.py --step restart
"""

import argparse
import datetime
import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path

# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
PROJECT_ROOT = Path(__file__).resolve().parent.parent
BASIC_DIR = PROJECT_ROOT / "basic"
SERVERSIDESCRIPT = Path(r"C:\nodesrunning\biznodeserversidescript")
LOG_DIR = PROJECT_ROOT / "logs"
LOG_DIR.mkdir(exist_ok=True)

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
log_file = LOG_DIR / f"maintenance_{datetime.date.today().isoformat()}.log"
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(str(log_file), encoding="utf-8"),
        logging.StreamHandler(sys.stdout),
    ],
)
log = logging.getLogger("maintenance")

# ---------------------------------------------------------------------------
# Core files/dirs to sync root → basic/
# Excludes: pro-only, admin-only, secrets, runtime data
# ---------------------------------------------------------------------------
SYNC_DIRS = [
    "automation",
    "backup",
    "config",
    "core",
    "docker",
    "identity",
    "scripts",
    "tools",
    "ui",
]

# Memory dir — only sync Python files, not Qdrant/Ollama binary data
MEMORY_SYNC_FILES = [
    "memory/__init__.py",
    "memory/database.py",
    "memory/db_adapter.py",
    "memory/qdrant_client.py",
    "memory/execution_memory.py",
    "memory/obsidian_layer.py",
]

# Files excluded from basic/ sync (pro-only or admin-only)
EXCLUDE_FROM_BASIC = {
    # Pro-only directories (never sync these to basic/)
    "bizchannel",
    "videoautomation",
    "voiceover",
    "tools/admin",
    "graphs",
    "logos",
    # Pro-only services
    "services/affiliate_service.py",
    "services/bizbrain_service.py",
    "services/crypto_service.py",
    "services/customer_engagement_service.py",
    "services/group_ad_service.py",
    "services/stripe_service.py",
    "services/promo_email_service.py",
    "services/transaction_service.py",
    "services/workflow_template_service.py",
    "services/webhook_sync_service.py",
    # Bot entry — admin run_bot.py has /mr, datavault, BizChannel admin
    # basic/ must maintain its own stripped run_bot.py
    "docker/bot/run_bot.py",
    # Pro-only root files
    "generate_voiceover.py",
    "render_video.py",
    "seed_architecture_kb.py",
    "seed_kb_admin.py",
    "seed_dzitsystem_kb.py",
    "NODE_DIVERGENCE.md",
    "TECHNICAL_DOCUMENTATION.md",
}

# Runtime / secret patterns to never sync
NEVER_SYNC = {
    ".env", ".env.txt", ".env.example",
    ".git", "__pycache__", ".qdrant-initialized",
    "bot.log", "dashboard.log",
    "backups", "logs", "dist", "output",
    "jobfiles", "restart", "snapshots",
    "basic", "pro",  # Never sync package dirs into themselves
    "node_modules", "channel.pdf",
}

# Files within synced dirs that should never be copied (secrets/runtime)
NEVER_SYNC_FILES = {
    "credentials.json", "token.json", "last_backup.json",
    "biznode.db", "biznode.db-journal", "biznode.db-wal",
    "youtube_client_secret.json", "youtube_token.json",
    "id_ed25519", "id_ed25519.pub",
}

# File extensions to never sync
NEVER_SYNC_EXT = {".pem", ".pyc", ".pyo", ".db", ".db-journal", ".db-wal"}

# Root-level files to sync
SYNC_ROOT_FILES = [
    "bot_runner.py",
    "launcher.py",
    "run_dashboard.py",
    "service_manager.py",
    "status.py",
    "seed_kb.py",
    "docker-compose.yml",
    "requirements.txt",
    "instruction.md",
    "INSTALL.txt",
    "setup.bat",
    "start.bat",
    # Legal (required in all tiers)
    "LICENSE",
    "DISCLAIMER.md",
    "LEGAL.md",
    "TERMS.md",
    "TRADEMARK.md",
]


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def run(cmd, cwd=None, check=True, capture=True):
    """Run a shell command and return output."""
    cwd = cwd or str(PROJECT_ROOT)
    log.debug("$ %s", cmd)
    result = subprocess.run(
        cmd, shell=True, cwd=cwd,
        capture_output=capture, text=True,
        timeout=300,
    )
    if check and result.returncode != 0:
        log.error("Command failed: %s\n%s", cmd, result.stderr)
    return result


def has_changes() -> bool:
    """Check if there are any uncommitted changes in the repo."""
    r = run("git status --porcelain")
    changed = bool(r.stdout.strip())
    if changed:
        lines = r.stdout.strip().split("\n")
        log.info("Found %d changed files", len(lines))
    else:
        log.info("No changes detected")
    return changed


def _should_exclude(rel_path: str) -> bool:
    """Check if a file should be excluded from basic/ sync."""
    rel_unix = rel_path.replace("\\", "/")
    parts = rel_unix.split("/")

    # Check never-sync patterns
    for part in parts:
        if part in NEVER_SYNC:
            return True
        if part.startswith(".") and part != ".gitkeep":
            return True
        if part == "__pycache__" or part.endswith(".pyc"):
            return True

    # Check sensitive files
    basename = parts[-1] if parts else ""
    if basename in NEVER_SYNC_FILES:
        return True
    # Check banned extensions
    _, ext = os.path.splitext(basename)
    if ext in NEVER_SYNC_EXT:
        return True
    # Skip credentials subdirectory
    if "credentials" in parts:
        return True

    # Check pro-only exclusions
    for pattern in EXCLUDE_FROM_BASIC:
        pattern = pattern.replace("\\", "/")
        if rel_unix == pattern or rel_unix.startswith(pattern + "/"):
            return True

    return False


# ---------------------------------------------------------------------------
# Steps
# ---------------------------------------------------------------------------

def step_sync(dry_run=False):
    """Sync core files from root → basic/."""
    log.info("=" * 60)
    log.info("STEP: Sync root -> basic/")
    log.info("=" * 60)

    synced = 0
    skipped = 0

    # Sync directories
    for dir_name in SYNC_DIRS:
        src_dir = PROJECT_ROOT / dir_name
        dst_dir = BASIC_DIR / dir_name

        if not src_dir.exists():
            continue

        for src_file in src_dir.rglob("*"):
            if src_file.is_dir():
                continue

            rel = src_file.relative_to(PROJECT_ROOT)
            rel_str = str(rel)

            if _should_exclude(rel_str):
                skipped += 1
                continue

            dst_file = BASIC_DIR / rel
            # Only copy if source is newer or destination doesn't exist
            if dst_file.exists():
                src_mtime = src_file.stat().st_mtime
                dst_mtime = dst_file.stat().st_mtime
                if src_mtime <= dst_mtime:
                    continue

            if dry_run:
                log.info("  [DRY] Would sync: %s", rel)
            else:
                dst_file.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(str(src_file), str(dst_file))
                log.info("  Synced: %s", rel)
            synced += 1

    # Sync memory Python files (not binary data)
    for rel_path in MEMORY_SYNC_FILES:
        src = PROJECT_ROOT / rel_path
        dst = BASIC_DIR / rel_path
        if not src.exists():
            continue
        if dst.exists() and src.stat().st_mtime <= dst.stat().st_mtime:
            continue
        if dry_run:
            log.info("  [DRY] Would sync: %s", rel_path)
        else:
            dst.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(str(src), str(dst))
            log.info("  Synced: %s", rel_path)
        synced += 1

    # Sync root-level files
    for filename in SYNC_ROOT_FILES:
        src = PROJECT_ROOT / filename
        dst = BASIC_DIR / filename

        if not src.exists():
            continue

        if dst.exists() and src.stat().st_mtime <= dst.stat().st_mtime:
            continue

        if dry_run:
            log.info("  [DRY] Would sync: %s", filename)
        else:
            shutil.copy2(str(src), str(dst))
            log.info("  Synced: %s", filename)
        synced += 1

    # Sync services/ separately (only core, not pro-only)
    svc_src = PROJECT_ROOT / "services"
    svc_dst = BASIC_DIR / "services"
    if svc_src.exists():
        for src_file in svc_src.iterdir():
            if src_file.is_dir() or not src_file.name.endswith(".py"):
                continue

            rel_str = f"services/{src_file.name}"
            if _should_exclude(rel_str):
                skipped += 1
                continue

            dst_file = svc_dst / src_file.name
            if dst_file.exists() and src_file.stat().st_mtime <= dst_file.stat().st_mtime:
                continue

            if dry_run:
                log.info("  [DRY] Would sync: %s", rel_str)
            else:
                svc_dst.mkdir(parents=True, exist_ok=True)
                shutil.copy2(str(src_file), str(dst_file))
                log.info("  Synced: %s", rel_str)
            synced += 1

    log.info("Sync complete: %d files synced, %d excluded", synced, skipped)
    return synced


def step_build(dry_run=False):
    """Rebuild BizNode.zip (basic package only, NEVER pro)."""
    log.info("=" * 60)
    log.info("STEP: Build basic package zip")
    log.info("=" * 60)

    output = str(SERVERSIDESCRIPT / "download" / "BizNode.zip")

    if dry_run:
        log.info("  [DRY] Would build: %s", output)
        return

    r = run(
        f'python scripts/build_package.py --tier basic --output "{output}"',
        cwd=str(PROJECT_ROOT),
    )
    if r.returncode == 0:
        size_mb = os.path.getsize(output) / (1024 * 1024)
        log.info("Built BizNode.zip: %.2f MB", size_mb)
    else:
        log.error("Build FAILED: %s", r.stderr)


def step_kb(dry_run=False):
    """Re-seed the support knowledge base."""
    log.info("=" * 60)
    log.info("STEP: Seed knowledge base")
    log.info("=" * 60)

    if dry_run:
        log.info("  [DRY] Would run: python scripts/seed_support_kb.py")
        return

    r = run("python scripts/seed_support_kb.py", cwd=str(PROJECT_ROOT))
    if r.returncode == 0:
        log.info("KB seeded successfully")
    else:
        log.error("KB seed FAILED: %s", r.stderr)


def step_commit(dry_run=False):
    """Git commit and push to private repo (origin)."""
    log.info("=" * 60)
    log.info("STEP: Git commit + push")
    log.info("=" * 60)

    if dry_run:
        log.info("  [DRY] Would commit and push")
        return

    today = datetime.date.today().strftime("%Y-%m-%d")

    # Stage all tracked changes (not untracked secrets)
    run("git add -A", cwd=str(PROJECT_ROOT))

    # Check if there's anything to commit
    r = run("git diff --cached --quiet", cwd=str(PROJECT_ROOT), check=False)
    if r.returncode == 0:
        log.info("Nothing to commit (staging area clean)")
        return

    # Commit
    msg = f"Daily maintenance: sync, build, seed KB — {today}"
    commit_cmd = (
        f'git commit -m "{msg}\n\n'
        f'Automated daily maintenance run.\n\n'
        f'Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"'
    )
    r = run(commit_cmd, cwd=str(PROJECT_ROOT))
    if r.returncode == 0:
        log.info("Committed: %s", msg)
    else:
        log.error("Commit failed: %s", r.stderr)
        return

    # Push to private repo only
    r = run("git push origin main", cwd=str(PROJECT_ROOT), check=False)
    if r.returncode == 0:
        log.info("Pushed to origin (private repo)")
    else:
        log.warning("Push failed (will retry next run): %s", r.stderr)


def step_deploy(dry_run=False):
    """Deploy website via update.ps1."""
    log.info("=" * 60)
    log.info("STEP: Deploy website")
    log.info("=" * 60)

    if not SERVERSIDESCRIPT.exists():
        log.error("Server-side script dir not found: %s", SERVERSIDESCRIPT)
        return

    if dry_run:
        log.info("  [DRY] Would run: update.ps1")
        return

    r = run(
        'powershell -ExecutionPolicy Bypass -File update.ps1',
        cwd=str(SERVERSIDESCRIPT),
        check=False,
    )
    if r.returncode == 0:
        log.info("Website deployed successfully")
    else:
        log.warning("Deploy had issues: %s", r.stderr)

    # Log upload summary
    if r.stdout:
        for line in r.stdout.split("\n"):
            if "uploaded" in line.lower() or "fail" in line.lower() or "All" in line:
                log.info("  %s", line.strip())


def step_restart(dry_run=False):
    """Restart the Telegram bot."""
    log.info("=" * 60)
    log.info("STEP: Restart bot")
    log.info("=" * 60)

    if dry_run:
        log.info("  [DRY] Would restart bot")
        return

    # Find and kill existing bot processes
    r = run(
        'powershell -Command "'
        'Get-Process python -ErrorAction SilentlyContinue | '
        'ForEach-Object { $id=$_.Id; '
        '$cmd=(Get-CimInstance Win32_Process -Filter \\\"ProcessId=$id\\\").CommandLine; '
        'if ($cmd -match \'bot_runner|run_bot\') { '
        'Stop-Process -Id $id -Force; Write-Output \\\"Killed PID $id\\\" } }"',
        cwd=str(PROJECT_ROOT),
        check=False,
    )
    if r.stdout:
        for line in r.stdout.strip().split("\n"):
            log.info("  %s", line.strip())

    # Start bot
    import time
    time.sleep(2)

    subprocess.Popen(
        ["python", "bot_runner.py"],
        cwd=str(PROJECT_ROOT),
        stdout=open(str(PROJECT_ROOT / "bot.log"), "a"),
        stderr=subprocess.STDOUT,
        creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
    )
    log.info("Bot restarted via bot_runner.py")

    # Wait and verify
    time.sleep(5)
    r = run(
        'powershell -Command "'
        'Get-Process python -ErrorAction SilentlyContinue | '
        'ForEach-Object { $id=$_.Id; '
        '$cmd=(Get-CimInstance Win32_Process -Filter \\\"ProcessId=$id\\\").CommandLine; '
        'if ($cmd -match \'run_bot\') { Write-Output \\\"Bot running: PID $id\\\" } }"',
        cwd=str(PROJECT_ROOT),
        check=False,
    )
    if r.stdout and "Bot running" in r.stdout:
        log.info("  %s", r.stdout.strip())
    else:
        log.warning("  Bot process not detected — check bot.log")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(description="BizNode Daily Maintenance")
    parser.add_argument("--dry-run", action="store_true",
                        help="Preview actions without executing")
    parser.add_argument("--force", action="store_true",
                        help="Run even if no changes detected")
    parser.add_argument("--step", type=str, default=None,
                        choices=["sync", "build", "kb", "commit", "deploy", "restart"],
                        help="Run a single step only")
    args = parser.parse_args()

    start_time = datetime.datetime.now()
    log.info("")
    log.info("=" * 60)
    log.info("  BizNode Daily Maintenance — %s", start_time.strftime("%Y-%m-%d %H:%M:%S"))
    log.info("  Mode: %s", "DRY RUN" if args.dry_run else "LIVE")
    log.info("=" * 60)

    # Single step mode
    if args.step:
        step_fn = {
            "sync": step_sync,
            "build": step_build,
            "kb": step_kb,
            "commit": step_commit,
            "deploy": step_deploy,
            "restart": step_restart,
        }[args.step]
        step_fn(dry_run=args.dry_run)
        return

    # Full run — check for changes first
    if not args.force and not has_changes():
        log.info("No changes detected. Skipping maintenance. Use --force to override.")
        return

    # Run all steps in order
    try:
        step_sync(dry_run=args.dry_run)
        step_build(dry_run=args.dry_run)
        step_kb(dry_run=args.dry_run)
        step_commit(dry_run=args.dry_run)
        step_deploy(dry_run=args.dry_run)
        step_restart(dry_run=args.dry_run)
    except Exception as exc:
        log.error("Maintenance FAILED: %s", exc, exc_info=True)

    elapsed = datetime.datetime.now() - start_time
    log.info("")
    log.info("=" * 60)
    log.info("  Maintenance complete — took %s", str(elapsed).split(".")[0])
    log.info("=" * 60)


if __name__ == "__main__":
    main()
