"""Linux cron job discovery.

Discovers user crontab jobs for all users on the system by enumerating
users from /etc/passwd and fetching each user's crontab via
'crontab -l -u <username>'. Requires root privileges (the agent is
always installed with sudo).

Only user-defined crontab entries are collected. System-level files such
as /etc/crontab and /etc/cron.d/ are intentionally excluded because they
typically contain default OS/distribution jobs that are not user-managed.
"""

import hashlib
import logging
import subprocess
from pathlib import Path
from typing import List, Dict, Any

logger = logging.getLogger(__name__)

# Shells that indicate a non-interactive / system account
_NOLOGIN_SHELLS = frozenset({
    "/usr/sbin/nologin",
    "/sbin/nologin",
    "/bin/false",
    "/usr/bin/false",
    "/bin/sync",
})


def _get_system_users() -> List[str]:
    """Return usernames that could own crontab entries.

    Reads /etc/passwd and filters out accounts whose login shell is a
    known non-interactive shell (nologin, false, sync).
    """
    users: List[str] = []
    passwd_path = Path("/etc/passwd")

    if not passwd_path.exists():
        return users

    try:
        with open(passwd_path, "r") as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                parts = line.split(":")
                if len(parts) < 7:
                    continue
                username = parts[0]
                shell = parts[6]

                # Skip accounts with non-interactive shells
                if shell in _NOLOGIN_SHELLS:
                    continue

                users.append(username)
    except Exception as e:
        logger.warning(f"Failed to read /etc/passwd: {e}")

    return users


def _parse_crontab_line(line: str, user: str) -> Dict[str, Any]:
    """Parse a single user-crontab line into a job dict.

    Expects the standard user crontab format produced by ``crontab -l``:
        minute hour day month weekday command
    """
    line = line.strip()

    # Skip comments and empty lines
    if not line or line.startswith("#"):
        return None

    # Skip environment variable assignments (e.g. SHELL=/bin/bash)
    if "=" in line and not line[0].isdigit() and line[0] != "*" and line[0] != "@":
        return None

    # Split into 5 schedule fields + command remainder
    parts = line.split(None, 5)
    if len(parts) < 6:
        return None

    schedule = " ".join(parts[:5])
    command = parts[5]

    # Generate fingerprint (include user for cross-user uniqueness)
    fingerprint_input = f"{schedule}|{command}|{user}"
    external_id = hashlib.sha256(fingerprint_input.encode()).hexdigest()

    # Generate command hash (for deduplication)
    command_hash = hashlib.sha256(command.encode()).hexdigest()[:12]

    # Generate job name from command
    name = command.split()[0] if command else "unknown"
    if "/" in name:
        name = Path(name).name

    return {
        "externalId": external_id,
        "name": name,
        "schedule": schedule,
        "source": "cron",
        "commandHash": command_hash,
        "location": f"crontab -u {user}",
        "enabled": True,
        "command": command,
        "user": user,
    }


def discover_cron_jobs() -> List[Dict[str, Any]]:
    """Discover user crontab jobs for every user on the system.

    Enumerates users from /etc/passwd (skipping system accounts) and
    fetches each user's crontab with ``crontab -l -u <username>``.
    The agent must be running as root to read other users' crontabs.
    """
    jobs: List[Dict[str, Any]] = []
    users = _get_system_users()

    if not users:
        # Fallback: if /etc/passwd cannot be read, try current user only
        logger.warning(
            "Could not enumerate system users, falling back to current user crontab"
        )
        users = ["root"]

    for user in users:
        try:
            result = subprocess.run(
                ["crontab", "-l", "-u", user],
                capture_output=True,
                text=True,
                timeout=5,
            )
            # returncode 0 means the user has a crontab
            if result.returncode == 0 and result.stdout.strip():
                for line in result.stdout.splitlines():
                    job = _parse_crontab_line(line, user)
                    if job:
                        jobs.append(job)
        except (subprocess.TimeoutExpired, FileNotFoundError):
            pass
        except Exception as e:
            logger.debug(f"Failed to read crontab for user '{user}': {e}")

    logger.info(f"Discovered {len(jobs)} cron job(s) across {len(users)} user(s)")
    return jobs

