import sqlite3
import json
import os
from datetime import datetime, timedelta

# ==================== CONFIGURATION ====================
CONFIG_PATH = "config.json"
DB_PATH = "database/jobs.db"
DASHBOARD_DIR = "outputs/dashboard"

# Charger la configuration
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
    config = json.load(f)

# Créer le dossier du dashboard
os.makedirs(DASHBOARD_DIR, exist_ok=True)

# ==================== RÉCUPÉRATION DES DONNÉES ====================

def get_jobs_from_db(days=30):
    """Récupérer les offres des derniers jours."""
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")
    cursor.execute("""
        SELECT title, company, location, salary, url, source, date, first_seen
        FROM jobs
        WHERE first_seen >= ?
        ORDER BY first_seen DESC
    """, (start_date,))
    jobs = []
    for row in cursor.fetchall():
        jobs.append({
            "title": row[0],
            "company": row[1],
            "location": row[2],
            "salary": row[3],
            "url": row[4],
            "source": row[5],
            "date": row[6],
            "first_seen": row[7]
        })
    conn.close()
    return jobs

def get_stats(jobs):
    """Calculer les statistiques."""
    total_jobs = len(jobs)
    sources = {}
    for job in jobs:
        sources[job["source"]] = sources.get(job["source"], 0) + 1
    locations = {}
    for job in jobs:
        locations[job["location"]] = locations.get(job["location"], 0) + 1
    new_jobs = [job for job in jobs 
                if datetime.strptime(job["first_seen"], "%Y-%m-%d %H:%M:%S") > 
                   (datetime.now() - timedelta(hours=24))]
    return {
        "total": total_jobs,
        "new_today": len(new_jobs),
        "sources": sources,
        "locations": dict(sorted(locations.items(), key=lambda x: x[1], reverse=True)[:10])
    }

# ==================== GÉNÉRATION HTML ====================

def generate_html(jobs, stats):
    """Générer le fichier index.html."""
    dashboard_config = config["dashboard_settings"]
    html = f"""<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{dashboard_config['title']} - {dashboard_config['subtitle']}</title>
    <link rel="stylesheet" href="style.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
    <div class="container">
        <header>
            <div class="header-content">
                <h1><i class="fas fa-search"></i> {dashboard_config['title']}</h1>
                <p class="subtitle">{dashboard_config['subtitle']}</p>
            </div>
            <div class="header-actions">
                <button id="theme-toggle" class="btn-theme" title="Changer de thème">
                    <i class="fas fa-moon"></i>
                </button>
                <div class="last-update">
                    <i class="fas fa-sync-alt"></i>
                    <span>{datetime.now().strftime("%d/%m/%Y à %H:%M")}</span>
                </div>
            </div>
        </header>

        <section class="stats">
            <div class="stat-card">
                <div class="stat-icon total"><i class="fas fa-briefcase"></i></div>
                <div class="stat-info">
                    <span class="stat-value">{stats['total']}</span>
                    <span class="stat-label">Offres totales</span>
                </div>
            </div>
            <div class="stat-card">
                <div class="stat-icon new"><i class="fas fa-star"></i></div>
                <div class="stat-info">
                    <span class="stat-value">{stats['new_today']}</span>
                    <span class="stat-label">Nouvelles aujourd'hui</span>
                </div>
            </div>
            <div class="stat-card">
                <div class="stat-icon sources"><i class="fas fa-globe"></i></div>
                <div class="stat-info">
                    <span class="stat-value">{len(stats['sources'])}</span>
                    <span class="stat-label">Sources actives</span>
                </div>
            </div>
        </section>

        <section class="filters">
            <h2><i class="fas fa-filter"></i> Filtres</h2>
            <div class="filter-group">
                <div class="filter-item">
                    <label for="source-filter"><i class="fas fa-building"></i> Source:</label>
                    <select id="source-filter">
                        <option value="all">Toutes les sources</option>
                        {"".join([f'<option value="{s}">{s}</option>' for s in stats['sources'].keys()])}
                    </select>
                </div>
                <div class="filter-item">
                    <label for="location-filter"><i class="fas fa-map-marker-alt"></i> Localisation:</label>
                    <select id="location-filter">
                        <option value="all">Toutes les localisations</option>
                        {"".join([f'<option value="{loc}">{loc}</option>' for loc in stats['locations'].keys()])}
                    </select>
                </div>
                <div class="filter-item">
                    <label for="sort-filter"><i class="fas fa-sort"></i> Trier par:</label>
                    <select id="sort-filter">
                        <option value="date-desc">Date (récent → ancien)</option>
                        <option value="date-asc">Date (ancien → récent)</option>
                        <option value="title-asc">Titre (A → Z)</option>
                        <option value="title-desc">Titre (Z → A)</option>
                    </select>
                </div>
            </div>
        </section>

        <section class="jobs-section">
            <h2><i class="fas fa-list"></i> Dernières offres</h2>
            <div class="jobs-container" id="jobs-container">
                {"".join([
                    f'<div class="job-card" data-source="{job["source"]}" data-location="{job["location"]}">' +
                    f'<div class="job-header"><div class="job-title-source">' +
                    f'<h3 class="job-title">{job["title"]}</h3>' +
                    f'<span class="job-source">{job["source"]}</span></div></div>' +
                    f'<div class="job-company"><i class="fas fa-building"></i> {job["company"]}</div>' +
                    f'<div class="job-details">' +
                    f'<div class="job-detail"><i class="fas fa-map-marker-alt"></i> {job["location"]}</div>' +
                    f'<div class="job-detail"><i class="fas fa-money-bill-wave"></i> {job["salary"] if job["salary"] else "Non spécifié"}</div>' +
                    f'<div class="job-detail"><i class="fas fa-calendar"></i> {datetime.strptime(job["first_seen"], "%Y-%m-%d %H:%M:%S").strftime("%d/%m/%Y")}</div>' +
                    f'</div>' +
                    f'<div class="job-actions">' +
                    f'<a href="{job["url"]}" target="_blank" class="btn-view">' +
                    f'<i class="fas fa-external-link-alt"></i> Voir l\'offre</a>' +
                    f'</div></div>'
                    for job in jobs
                ])}
            </div>
        </section>

        <section class="charts-section">
            <h2><i class="fas fa-chart-pie"></i> Statistiques</h2>
            <div class="charts-container">
                <div class="chart-card">
                    <h3><i class="fas fa-chart-pie"></i> Répartition par source</h3>
                    <canvas id="sourcesChart"></canvas>
                </div>
                <div class="chart-card">
                    <h3><i class="fas fa-chart-bar"></i> Répartition par localisation</h3>
                    <canvas id="locationsChart"></canvas>
                </div>
            </div>
        </section>

        <footer>
            <p>Généré automatiquement par <strong>Job Scanner</strong> | 
               <a href="#" onclick="window.location.reload()"><i class="fas fa-sync"></i> Rafraîchir</a></p>
        </footer>
    </div>
    <script>
        const jobsData = {json.dumps(jobs)};
        const statsData = {json.dumps(stats)};
    </script>
    <script src="script.js"></script>
</body>
</html>
"""
    with open(f"{DASHBOARD_DIR}/index.html", "w", encoding="utf-8") as f:
        f.write(html)
    print("✅ index.html généré")

# ==================== GÉNÉRATION CSS ====================

def generate_css():
    """Générer le fichier style.css."""
    css = """
:root {
    --primary: #3498db; --secondary: #2980b9; --success: #2ecc71;
    --warning: #f39c12; --danger: #e74c3c; --dark: #2c3e50; --light: #ecf0f1;
    --bg: #f8f9fa; --text: #333; --card-bg: #ffffff;
    --border-radius: 8px; --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    --transition: all 0.3s ease;
}
[data-theme="dark"] {
    --bg: #1a1a1a; --text: #e0e0e0; --card-bg: #2c3e50;
    --shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh;
}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
header {
    display: flex; justify-content: space-between; align-items: center;
    margin-bottom: 30px; padding: 20px 0; border-bottom: 1px solid var(--primary);
    flex-wrap: wrap; gap: 15px;
}
.header-content h1 { color: var(--primary); font-size: 1.8rem; margin-bottom: 5px; }
.header-content .subtitle { color: var(--secondary); font-size: 1rem; }
.header-actions { display: flex; align-items: center; gap: 15px; }
.btn-theme {
    background: var(--card-bg); border: 1px solid var(--primary); color: var(--primary);
    padding: 8px 12px; border-radius: var(--border-radius); cursor: pointer;
    transition: var(--transition);
}
.btn-theme:hover { background: var(--primary); color: white; }
.last-update { color: var(--text); font-size: 0.9rem; opacity: 0.8; display: flex; align-items: center; gap: 5px; }
.stats {
    display: flex; justify-content: space-around; flex-wrap: wrap; gap: 20px;
    margin-bottom: 40px;
}
.stat-card {
    background: var(--card-bg); border-radius: var(--border-radius); padding: 20px;
    min-width: 200px; flex: 1; box-shadow: var(--shadow); display: flex;
    align-items: center; gap: 15px; transition: var(--transition);
}
.stat-card:hover { transform: translateY(-5px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); }
.stat-icon {
    font-size: 2rem; width: 50px; height: 50px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
}
.stat-icon.total { background: rgba(52, 152, 219, 0.2); color: var(--primary); }
.stat-icon.new { background: rgba(46, 204, 113, 0.2); color: var(--success); }
.stat-icon.sources { background: rgba(243, 156, 18, 0.2); color: var(--warning); }
.stat-info { text-align: center; }
.stat-value { display: block; font-size: 1.8rem; font-weight: bold; color: var(--primary); }
.stat-label { display: block; font-size: 0.85rem; opacity: 0.8; margin-top: 5px; }
.filters {
    background: var(--card-bg); border-radius: var(--border-radius); padding: 20px;
    margin-bottom: 30px; box-shadow: var(--shadow);
}
.filters h2 { color: var(--primary); margin-bottom: 15px; font-size: 1.3rem; display: flex; align-items: center; gap: 10px; }
.filter-group { display: flex; flex-wrap: wrap; gap: 15px; }
.filter-item { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 150px; }
.filter-item label { font-weight: 600; font-size: 0.9rem; display: flex; align-items: center; gap: 5px; }
.filter-item select {
    padding: 8px 12px; border: 1px solid var(--primary); border-radius: var(--border-radius);
    background: var(--card-bg); color: var(--text); font-size: 0.95rem;
}
.filter-item select:focus { outline: none; border-color: var(--secondary); }
.jobs-section { margin-bottom: 40px; }
.jobs-section h2 { color: var(--primary); margin-bottom: 20px; font-size: 1.5rem; display: flex; align-items: center; gap: 10px; }
.jobs-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; }
.job-card {
    background: var(--card-bg); border-radius: var(--border-radius); padding: 20px;
    box-shadow: var(--shadow); transition: var(--transition); border-left: 4px solid var(--primary);
    animation: fadeIn 0.5s ease forwards;
}
.job-card:hover { transform: translateY(-3px); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); }
.job-header { margin-bottom: 10px; }
.job-title-source { display: flex; justify-content: space-between; align-items: flex-start; }
.job-title { color: var(--primary); font-size: 1.1rem; margin-bottom: 5px; flex: 1; line-height: 1.3; }
.job-source {
    background: var(--primary); color: white; padding: 4px 8px; border-radius: 4px;
    font-size: 0.7rem; font-weight: 600; white-space: nowrap;
}
.job-company { color: var(--text); font-weight: 600; margin-bottom: 15px; font-size: 0.95rem; display: flex; align-items: center; gap: 8px; }
.job-details { display: flex; flex-direction: column; gap: 8px; margin-bottom: 15px; }
.job-detail { display: flex; align-items: center; font-size: 0.85rem; color: var(--text); opacity: 0.9; }
.job-detail i { margin-right: 8px; color: var(--secondary); width: 16px; }
.job-actions { display: flex; justify-content: flex-end; }
.btn-view {
    background: var(--primary); color: white; padding: 8px 16px; border-radius: var(--border-radius);
    text-decoration: none; font-weight: 600; transition: var(--transition);
    display: inline-flex; align-items: center; gap: 5px; font-size: 0.9rem;
}
.btn-view:hover { background: var(--secondary); transform: translateY(-2px); }
.charts-section { margin-bottom: 40px; }
.charts-section h2 { color: var(--primary); margin-bottom: 20px; font-size: 1.5rem; display: flex; align-items: center; gap: 10px; }
.charts-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
.chart-card {
    background: var(--card-bg); border-radius: var(--border-radius); padding: 20px;
    box-shadow: var(--shadow);
}
.chart-card h3 { color: var(--primary); margin-bottom: 15px; font-size: 1.1rem; display: flex; align-items: center; gap: 8px; }
footer {
    text-align: center; padding: 20px; margin-top: 30px;
    border-top: 1px solid var(--primary); color: var(--text); opacity: 0.7;
}
footer a { color: var(--primary); text-decoration: none; }
footer a:hover { text-decoration: underline; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.no-jobs { text-align: center; padding: 40px; color: var(--text); opacity: 0.7; grid-column: 1 / -1; }
@media (max-width: 768px) {
    .container { padding: 10px; }
    header { flex-direction: column; text-align: center; }
    .header-actions { flex-direction: column; }
    .stats { flex-direction: column; }
    .stat-card { min-width: 100%; }
    .jobs-container { grid-template-columns: 1fr; }
    .charts-container { grid-template-columns: 1fr; }
    .filter-group { flex-direction: column; }
    .filter-item { width: 100%; }
}
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: var(--light); }
::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 5px; }
"""
    with open(f"{DASHBOARD_DIR}/style.css", "w", encoding="utf-8") as f:
        f.write(css)
    print("✅ style.css généré")

def generate_javascript():
    """Générer le fichier script.js."""
    js = """
const allJobs = jobsData;
const stats = statsData;
function filterJobs() {
    const sourceFilter = document.getElementById('source-filter').value;
    const locationFilter = document.getElementById('location-filter').value;
    const sortFilter = document.getElementById('sort-filter').value;
    let filteredJobs = [...allJobs];
    if (sourceFilter !== 'all') filteredJobs = filteredJobs.filter(job => job.source === sourceFilter);
    if (locationFilter !== 'all') filteredJobs = filteredJobs.filter(job => job.location === locationFilter);
    if (sortFilter === 'date-desc') filteredJobs.sort((a, b) => new Date(b.first_seen) - new Date(a.first_seen));
    else if (sortFilter === 'date-asc') filteredJobs.sort((a, b) => new Date(a.first_seen) - new Date(b.first_seen));
    else if (sortFilter === 'title-asc') filteredJobs.sort((a, b) => a.title.localeCompare(b.title));
    else if (sortFilter === 'title-desc') filteredJobs.sort((a, b) => b.title.localeCompare(a.title));
    displayJobs(filteredJobs);
}
function displayJobs(jobs) {
    const container = document.getElementById('jobs-container');
    if (jobs.length === 0) {
        container.innerHTML = '<div class="no-jobs"><i class="fas fa-inbox"></i><p>Aucune offre ne correspond à vos filtres.</p></div>';
        return;
    }
    container.innerHTML = jobs.map((job, index) => `
        <div class="job-card" data-source="${job.source}" data-location="${job.location}" style="animation-delay: ${index * 0.1}s">
            <div class="job-header">
                <div class="job-title-source">
                    <h3 class="job-title">${job.title}</h3>
                    <span class="job-source">${job.source}</span>
                </div>
            </div>
            <div class="job-company"><i class="fas fa-building"></i> ${job.company}</div>
            <div class="job-details">
                <div class="job-detail"><i class="fas fa-map-marker-alt"></i> ${job.location}</div>
                <div class="job-detail"><i class="fas fa-money-bill-wave"></i> ${job.salary || 'Non spécifié'}</div>
                <div class="job-detail"><i class="fas fa-calendar"></i> ${new Date(job.first_seen).toLocaleDateString('fr-FR')}</div>
            </div>
            <div class="job-actions">
                <a href="${job.url}" target="_blank" class="btn-view">
                    <i class="fas fa-external-link-alt"></i> Voir l'offre
                </a>
            </div>
        </div>
    `).join('');
}
function initCharts() {
    const chartColors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c', '#16a085', '#27ae60'];
    const sourcesCtx = document.getElementById('sourcesChart');
    if (sourcesCtx) {
        const sources = Object.entries(stats.sources);
        new Chart(sourcesCtx, {
            type: 'doughnut',
            data: {
                labels: sources.map(([k, v]) => k),
                datasets: [{
                    data: sources.map(([k, v]) => v),
                    backgroundColor: chartColors.slice(0, sources.length),
                    borderWidth: 1
                }]
            },
            options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
        });
    }
    const locationsCtx = document.getElementById('locationsChart');
    if (locationsCtx) {
        const locations = Object.entries(stats.locations);
        new Chart(locationsCtx, {
            type: 'bar',
            data: {
                labels: locations.map(([k, v]) => k),
                datasets: [{
                    label: 'Nombre d\'offres',
                    data: locations.map(([k, v]) => v),
                    backgroundColor: chartColors.slice(0, locations.length)
                }]
            },
            options: {
                responsive: true,
                plugins: { legend: { display: false } },
                scales: { y: { beginAtZero: true } }
            }
        });
    }
}
function toggleTheme() {
    const html = document.documentElement;
    const currentTheme = html.getAttribute('data-theme');
    const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
    html.setAttribute('data-theme', newTheme);
    localStorage.setItem('theme', newTheme);
    document.getElementById('theme-toggle').innerHTML = newTheme === 'dark' ? '<i class="fas fa-sun"></i>' : '<i class="fas fa-moon"></i>';
}
if (localStorage.getItem('theme') === 'dark') {
    document.documentElement.setAttribute('data-theme', 'dark');
    document.getElementById('theme-toggle').innerHTML = '<i class="fas fa-sun"></i>';
}
if (document.getElementById('source-filter')) document.getElementById('source-filter').addEventListener('change', filterJobs);
if (document.getElementById('location-filter')) document.getElementById('location-filter').addEventListener('change', filterJobs);
if (document.getElementById('sort-filter')) document.getElementById('sort-filter').addEventListener('change', filterJobs);
if (document.getElementById('theme-toggle')) document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
window.addEventListener('DOMContentLoaded', () => { filterJobs(); initCharts(); });
"""
    with open(f"{DASHBOARD_DIR}/script.js", "w", encoding="utf-8") as f:
        f.write(js)
    print("✅ script.js généré")

# ==================== FONCTION PRINCIPALE ====================

def main():
    print("📊 Génération du dashboard...")
    jobs = get_jobs_from_db(days=30)
    stats = get_stats(jobs)
    generate_html(jobs, stats)
    generate_css()
    generate_javascript()
    print(f"✅ Dashboard généré dans {DASHBOARD_DIR}/")
    print(f"   - {len(jobs)} offres affichées")
    print(f"   - {stats['new_today']} nouvelles aujourd'hui")

if __name__ == "__main__":
    main()
"""

---

## 📤 **Fichier 4 : `upload_to_ftp.py`**

> **Upload le dashboard vers ton serveur FTP automatiquement.**

```python
import os
import json
from ftplib import FTP, FTP_TLS
import ssl

# ==================== CONFIGURATION ====================
CONFIG_PATH = "config.json"
DASHBOARD_DIR = "outputs/dashboard"

# Charger la configuration
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
    config = json.load(f)

ftp_config = config["ftp_settings"]

# ==================== UPLOAD FTP ====================

def upload_to_ftp():
    """Uploader le dossier dashboard vers le serveur FTP."""
    if not ftp_config.get("enabled", False):
        print("⚠️ Upload FTP désactivé dans la configuration")
        return
    
    print("📤 Début de l'upload FTP...")
    
    try:
        # Créer une connexion FTP
        if ftp_config.get("port") == 21:
            ftp = FTP()
        else:
            ftp = FTP_TLS(context=ssl._create_default_https_context())
        
        # Connexion au serveur
        ftp.connect(
            host=ftp_config["host"],
            port=ftp_config.get("port", 21),
            timeout=10
        )
        print(f"✅ Connecté à {ftp_config['host']}:{ftp_config.get('port', 21)}")
        
        # Authentification
        ftp.login(
            user=ftp_config["username"],
            passwd=ftp_config["password"]
        )
        print("✅ Authentification réussie")
        
        # Changer de répertoire distant
        remote_path = ftp_config["remote_path"]
        try:
            ftp.cwd(remote_path)
            print(f"✅ Répertoire distant: {remote_path}")
        except Exception as e:
            print(f"⚠️ Répertoire {remote_path} introuvable, création...")
            # Créer les répertoires
            for dir_name in remote_path.split('/')[1:]:
                if dir_name:
                    try:
                        ftp.cwd(dir_name)
                    except:
                        ftp.mkd(dir_name)
                        ftp.cwd(dir_name)
        
        # Lister les fichiers locaux à uploader
        local_dir = ftp_config.get("local_dashboard_dir", DASHBOARD_DIR)
        for root, dirs, files in os.walk(local_dir):
            for file in files:
                local_path = os.path.join(root, file)
                relative_path = os.path.relpath(local_path, local_dir)
                
                # Naviguer vers le bon répertoire distant
                remote_file_path = os.path.join(remote_path, relative_path)
                remote_dir = os.path.dirname(remote_file_path)
                
                # Créer les sous-répertoires si nécessaire
                if remote_dir != remote_path:
                    try:
                        ftp.cwd(remote_dir)
                    except:
                        # Créer le chemin complet
                        current = remote_path
                        for part in remote_dir.replace(remote_path, '').split('/'):
                            if part:
                                try:
                                    ftp.cwd(os.path.join(current, part))
                                except:
                                    ftp.mkd(os.path.join(current, part))
                                    ftp.cwd(os.path.join(current, part))
                                current = os.path.join(current, part)
                
                # Uploader le fichier
                with open(local_path, 'rb') as f:
                    ftp.storbinary(f'STOR {os.path.basename(remote_file_path)}', f)
                print(f"✅ Upload: {relative_path}")
        
        # Fermer la connexion
        ftp.quit()
        print("✅ Upload FTP terminé avec succès !")
        domain = ftp_config["host"].replace("ftp.", "").replace("http://", "").replace("https://", "")
        print(f"🌐 Dashboard accessible à: https://{domain}{remote_path}")
        
    except Exception as e:
        print(f"❌ Erreur FTP: {e}")
        if 'ftp' in locals():
            try:
                ftp.quit()
            except:
                pass

if __name__ == "__main__":
    upload_to_ftp()
"""

---

## 🐍 **Fichier 5 : `job_scanner.py` (version finale)**

> **Script principal mis à jour pour générer le dashboard et l'uploader.**

```python
import json
import sqlite3
import smtplib
import os
import subprocess
import requests
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime, timedelta
import re
import hashlib

# ==================== CONFIGURATION ====================
CONFIG_PATH = "config.json"
DB_PATH = "database/jobs.db"
OUTPUT_DIR = "outputs"
DASHBOARD_DIR = "outputs/dashboard"

# Charger la configuration
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
    config = json.load(f)

# Créer les dossiers
os.makedirs("database", exist_ok=True)
os.makedirs(OUTPUT_DIR, exist_ok=True)
os.makedirs(DASHBOARD_DIR, exist_ok=True)

# ==================== BASE DE DONNÉES ====================

def init_db():
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS jobs (
            id TEXT PRIMARY KEY,
            title TEXT, company TEXT, location TEXT, salary TEXT,
            url TEXT UNIQUE, source TEXT, date TEXT, description TEXT,
            first_seen TEXT
        )
    """)
    conn.commit()
    return conn

def generate_job_id(title, company, url):
    return hashlib.md5(f"{title}{company}{url}".encode("utf-8")).hexdigest()

def is_near_location(location, cities):
    if not location:
        return False
    location_lower = location.lower()
    for city in cities:
        if city.lower() in location_lower:
            return True
    nearby_keywords = ["antibes", "sophia antipolis", "valbonne", "biot", "mougins", 
                      "cannes", "nice", "cagnes", "vallauris", "le cannet"]
    for keyword in nearby_keywords:
        if keyword in location_lower:
            return True
    return False

def contains_job_title(text, titles):
    if not text:
        return False
    text_lower = text.lower()
    for title in titles:
        normalized = title.lower()
        for a, r in [('é','e'),('è','e'),('ê','e'),('à','a'),('â','a'),('î','i'),('ï','i'),('ô','o'),('ö','o'),('ù','u'),('û','u'),('ç','c')]:
            normalized = normalized.replace(a, r)
        if normalized in text_lower:
            return True
    return False

def contains_excluded_keywords(text, exclude_keywords):
    if not text or not exclude_keywords:
        return False
    text_lower = text.lower()
    for keyword in exclude_keywords:
        if keyword.lower() in text_lower:
            return True
    return False

def get_current_date():
    return datetime.now().strftime("%Y-%m-%d")

def get_current_datetime():
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# ==================== SCAN DES PLATEFORMES ====================

def scan_platform(platform_name, base_url, search_params, criteria):
    """Fonction générique de scan pour une plateforme."""
    jobs = []
    try:
        headers = {"User-Agent": criteria["user_agent"]}
        response = requests.get(search_params["url"], headers=headers, timeout=10)
        if response.status_code != 200:
            return jobs
        
        soup = BeautifulSoup(response.text, "html.parser")
        job_cards = soup.find_all("div", class_=re.compile(search_params["card_class"]))
        
        for card in job_cards[:criteria["max_results_per_platform"]]:
            try:
                title_elem = card.find(search_params["title_tag"], class_=re.compile(search_params["title_class"]))
                title = title_elem.get_text(strip=True) if title_elem else ""
                
                company_elem = card.find(search_params["company_tag"], class_=re.compile(search_params["company_class"]))
                company = company_elem.get_text(strip=True) if company_elem else ""
                
                location_elem = card.find(search_params["location_tag"], class_=re.compile(search_params["location_class"]))
                location = location_elem.get_text(strip=True) if location_elem else ""
                
                salary_elem = card.find(search_params["salary_tag"], class_=re.compile(search_params["salary_class"]))
                salary = salary_elem.get_text(strip=True) if salary_elem else ""
                
                link_elem = card.find("a", href=True)
                url = base_url + link_elem["href"] if link_elem else ""
                
                date_elem = card.find(search_params["date_tag"], class_=re.compile(search_params["date_class"]))
                date = date_elem.get_text(strip=True) if date_elem else ""
                
                if not contains_job_title(title, criteria["job_titles_fr"] + criteria["job_titles_en"]):
                    continue
                if not is_near_location(location, criteria["cities_nearby"]):
                    continue
                if contains_excluded_keywords(title + " " + company, criteria["exclude_keywords"]):
                    continue
                
                jobs.append({
                    "title": title, "company": company, "location": location,
                    "salary": salary, "url": url, "source": platform_name,
                    "date": date, "description": ""
                })
            except Exception as e:
                print(f"⚠️ Erreur parsing {platform_name}: {e}")
                continue
    except Exception as e:
        print(f"⚠️ Erreur scan {platform_name}: {e}")
    
    return jobs

def scan_indeed():
    criteria = config["search_criteria"]
    base_url = "https://fr.indeed.com"
    jobs = []
    for title in criteria["job_titles_fr"] + criteria["job_titles_en"]:
        search_query = f"{title} {criteria['location']}".replace(" ", "+")
        url = f"{base_url}/jobs?q={search_query}&l=Antibes%2C+Alpes-Maritimes"
        jobs.extend(scan_platform("Indeed", base_url, {
            "url": url,
            "card_class": r"job_seen_beacon",
            "title_tag": "h2", "title_class": r"jobTitle",
            "company_tag": "span", "company_class": r"companyName",
            "location_tag": "div", "location_class": r"companyLocation",
            "salary_tag": "div", "salary_class": r"salary-snippet",
            "date_tag": "span", "date_class": r"date"
        }, config["scan_settings"]))
    return jobs

def scan_ape():
    criteria = config["search_criteria"]
    base_url = "https://www.apec.fr"
    jobs = []
    for title in criteria["job_titles_fr"]:
        search_query = f"{title} {criteria['location']}".replace(" ", "+")
        url = f"{base_url}/candidat/recherche-emploi.html?motscles={search_query}&lieux=Antibes%2C+Sophia+Antipolis"
        jobs.extend(scan_platform("APEC", base_url, {
            "url": url,
            "card_class": r"offre",
            "title_tag": "h2", "title_class": r".*",
            "company_tag": "span", "company_class": r"entreprise",
            "location_tag": "span", "location_class": r"localite",
            "salary_tag": "span", "salary_class": r"salaire",
            "date_tag": "span", "date_class": r"date"
        }, config["scan_settings"]))
    return jobs

def scan_linkedin():
    criteria = config["search_criteria"]
    jobs = []
    try:
        from googlesearch import search
    except ImportError:
        print("⚠️ Installation de googlesearch-python...")
        os.system("pip install googlesearch-python")
        from googlesearch import search
    
    for title in criteria["job_titles_fr"] + criteria["job_titles_en"]:
        try:
            query = f"site:linkedin.com/jobs {title} {criteria['location']}"
            for result in search(query, num_results=10, stop=10, pause=2):
                if "linkedin.com/jobs/view/" in result:
                    jobs.append({
                        "title": title, "company": "", "location": criteria["location"],
                        "salary": "", "url": result, "source": "LinkedIn",
                        "date": get_current_date(), "description": ""
                    })
        except Exception as e:
            print(f"⚠️ Erreur scan LinkedIn: {e}")
    
    # Récupérer les détails
    detailed_jobs = []
    for job in jobs:
        try:
            headers = {"User-Agent": criteria["user_agent"]}
            response = requests.get(job["url"], headers=headers, timeout=10)
            if response.status_code != 200:
                continue
            soup = BeautifulSoup(response.text, "html.parser")
            title_elem = soup.find("h1", class_=re.compile(r"top-card-layout__title"))
            title = title_elem.get_text(strip=True) if title_elem else job["title"]
            company_elem = soup.find("a", class_=re.compile(r"topcard__org-name-link"))
            company = company_elem.get_text(strip=True) if company_elem else ""
            location_elem = soup.find("span", class_=re.compile(r"topcard__flavor-row-item"))
            location = location_elem.get_text(strip=True) if location_elem else ""
            
            if not contains_job_title(title, criteria["job_titles_fr"] + criteria["job_titles_en"]):
                continue
            if not is_near_location(location, criteria["cities_nearby"]):
                continue
            if contains_excluded_keywords(title + " " + company, criteria["exclude_keywords"]):
                continue
            
            detailed_jobs.append({
                "title": title, "company": company, "location": location,
                "salary": "", "url": job["url"], "source": "LinkedIn",
                "date": get_current_date(), "description": ""
            })
        except Exception as e:
            print(f"⚠️ Erreur détails LinkedIn: {e}")
    return detailed_jobs

def scan_welcome_to_the_jungle():
    criteria = config["search_criteria"]
    base_url = "https://www.welcometothejungle.com"
    jobs = []
    for title in criteria["job_titles_fr"] + criteria["job_titles_en"]:
        search_query = title.replace(" ", "-")
        url = f"{base_url}/fr/jobs?query={search_query}&location=Antibes%2C+Sophia+Antipolis"
        jobs.extend(scan_platform("Welcome to the Jungle", base_url, {
            "url": url,
            "card_class": r"job-card",
            "title_tag": "h3", "title_class": r".*",
            "company_tag": "span", "company_class": r"company-name",
            "location_tag": "span", "location_class": r"location",
            "salary_tag": "span", "salary_class": r"salary",
            "date_tag": "div", "date_class": r".*"
        }, config["scan_settings"]))
    return jobs

def scan_monster():
    criteria = config["search_criteria"]
    base_url = "https://www.monster.fr"
    jobs = []
    for title in criteria["job_titles_fr"] + criteria["job_titles_en"]:
        search_query = f"{title} {criteria['location']}".replace(" ", "+")
        url = f"{base_url}/emploi/recherche/?q={search_query}&where=Antibes"
        jobs.extend(scan_platform("Monster", base_url, {
            "url": url,
            "card_class": r"job-card",
            "title_tag": "h2", "title_class": r".*",
            "company_tag": "span", "company_class": r"company-name",
            "location_tag": "span", "location_class": r"location",
            "salary_tag": "span", "salary_class": r"salary",
            "date_tag": "span", "date_class": r"date"
        }, config["scan_settings"]))
    return jobs

def scan_pole_emploi():
    criteria = config["search_criteria"]
    base_url = "https://candidat.pole-emploi.fr"
    jobs = []
    for title in criteria["job_titles_fr"]:
        search_query = title.replace(" ", "+")
        url = f"{base_url}/offres/recherche?motsCle={search_query}&lieux=Antibes%2C+Sophia+Antipolis"
        jobs.extend(scan_platform("Pôle Emploi", base_url, {
            "url": url,
            "card_class": r"offre",
            "title_tag": "h2", "title_class": r".*",
            "company_tag": "span", "company_class": r"entreprise",
            "location_tag": "span", "location_class": r"lieu",
            "salary_tag": "span", "salary_class": r"salaire",
            "date_tag": "span", "date_class": r"date"
        }, config["scan_settings"]))
    return jobs

def scan_all_platforms():
    all_jobs = []
    criteria = config["search_criteria"]
    platforms = criteria["platforms"]
    print(f"[{get_current_datetime()}] Début du scan...")
    
    if platforms.get("indeed", True):
        print(f"[{get_current_datetime()}] Scan Indeed...")
        all_jobs.extend(scan_indeed())
        print(f"[{get_current_datetime()}] Indeed: {len([j for j in all_jobs if j['source'] == 'Indeed'])} offres")
    if platforms.get("ape", True):
        print(f"[{get_current_datetime()}] Scan APEC...")
        all_jobs.extend(scan_ape())
        print(f"[{get_current_datetime()}] APEC: {len([j for j in all_jobs if j['source'] == 'APEC'])} offres")
    if platforms.get("linkedin", True):
        print(f"[{get_current_datetime()}] Scan LinkedIn...")
        all_jobs.extend(scan_linkedin())
        print(f"[{get_current_datetime()}] LinkedIn: {len([j for j in all_jobs if j['source'] == 'LinkedIn'])} offres")
    if platforms.get("welcome_to_the_jungle", True):
        print(f"[{get_current_datetime()}] Scan Welcome to the Jungle...")
        all_jobs.extend(scan_welcome_to_the_jungle())
        print(f"[{get_current_datetime()}] WTTJ: {len([j for j in all_jobs if j['source'] == 'Welcome to the Jungle'])} offres")
    if platforms.get("monster", True):
        print(f"[{get_current_datetime()}] Scan Monster...")
        all_jobs.extend(scan_monster())
        print(f"[{get_current_datetime()}] Monster: {len([j for j in all_jobs if j['source'] == 'Monster'])} offres")
    if platforms.get("pole_emploi", True):
        print(f"[{get_current_datetime()}] Scan Pôle Emploi...")
        all_jobs.extend(scan_pole_emploi())
        print(f"[{get_current_datetime()}] Pôle Emploi: {len([j for j in all_jobs if j['source'] == 'Pôle Emploi'])} offres")
    
    print(f"[{get_current_datetime()}] Scan terminé. Total: {len(all_jobs)} offres brutes")
    return all_jobs

# ==================== FILTRAGE ET SAUVEGARDE ====================

def filter_and_save_jobs(all_jobs):
    conn = init_db()
    cursor = conn.cursor()
    new_jobs = []
    updated_jobs = []
    for job in all_jobs:
        job_id = generate_job_id(job["title"], job["company"], job["url"])
        cursor.execute("SELECT id FROM jobs WHERE id = ?", (job_id,))
        exists = cursor.fetchone()
        if not exists:
            cursor.execute("""
                INSERT INTO jobs (id, title, company, location, salary, url, source, date, description, first_seen)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """, (job_id, job["title"], job["company"], job["location"], job["salary"],
                  job["url"], job["source"], job["date"], job["description"], get_current_datetime()))
            new_jobs.append(job)
        else:
            cursor.execute("SELECT first_seen FROM jobs WHERE id = ?", (job_id,))
            first_seen = cursor.fetchone()[0]
            first_seen_date = datetime.strptime(first_seen, "%Y-%m-%d %H:%M:%S")
            if (datetime.now() - first_seen_date).days >= 7:
                cursor.execute("UPDATE jobs SET first_seen = ? WHERE id = ?", (get_current_datetime(), job_id))
                updated_jobs.append(job)
    conn.commit()
    conn.close()
    return new_jobs, updated_jobs

def export_to_csv(jobs, filename=None):
    if not filename:
        filename = f"{OUTPUT_DIR}/offres_{get_current_date()}.csv"
    df = pd.DataFrame(jobs)
    df.to_csv(filename, index=False, encoding="utf-8-sig")
    return filename

# ==================== NOTIFICATIONS EMAIL ====================

def send_email_notification(new_jobs, updated_jobs):
    email_config = config["email_settings"]
    user = config["user"]
    if not new_jobs and not updated_jobs:
        print(f"[{get_current_datetime()}] Aucune nouvelle offre à notifier")
        return
    
    subject = f"[Job Scanner] {len(new_jobs)} nouvelles offres ({get_current_date()})"
    if updated_jobs:
        subject += f" + {len(updated_jobs)} mises à jour"
    
    body = f"Bonjour {user['name']},\n\n"
    body += f"Voici les nouvelles offres pour aujourd'hui, {get_current_date()} :\n\n"
    
    if new_jobs:
        body += f"--- NOUVELLES OFFRES ({len(new_jobs)}) ---\n\n"
        for i, job in enumerate(new_jobs, 1):
            body += f"{i}. {job['title']}\n"
            body += f"   Entreprise : {job['company']}\n"
            body += f"   Localisation : {job['location']}\n"
            body += f"   Salaire : {job['salary']}\n"
            body += f"   Source : {job['source']}\n"
            body += f"   Lien : {job['url']}\n\n"
    
    if updated_jobs:
        body += f"--- MISES À JOUR ({len(updated_jobs)}) ---\n\n"
        for i, job in enumerate(updated_jobs, 1):
            body += f"{i}. {job['title']}\n"
            body += f"   Lien : {job['url']}\n\n"
    
    body += f"Dashboard: https://{config['ftp_settings']['host'].replace('ftp.', '')}{config['ftp_settings']['remote_path']}\n"
    body += "Bonne recherche !\n"
    
    msg = MIMEMultipart()
    msg["From"] = email_config["from_email"]
    msg["To"] = email_config["to_email"]
    msg["Subject"] = subject
    msg.attach(MIMEText(body, "plain", "utf-8"))
    
    try:
        with smtplib.SMTP(email_config["smtp_server"], email_config["smtp_port"]) as server:
            server.starttls()
            server.login(email_config["smtp_username"], email_config["smtp_password"])
            server.send_message(msg)
        print(f"[{get_current_datetime()}] Email envoyé à {email_config['to_email']}")
    except Exception as e:
        print(f"[{get_current_datetime()}] Échec email: {e}")

# ==================== FONCTION PRINCIPALE ====================

def main():
    print("=" * 60)
    print("JOB SCANNER - IT Operations/Infrastructure")
    print("=" * 60)
    
    all_jobs = scan_all_platforms()
    new_jobs, updated_jobs = filter_and_save_jobs(all_jobs)
    print(f"[{get_current_datetime()}] {len(new_jobs)} nouvelles offres, {len(updated_jobs)} mises à jour")
    
    if new_jobs or updated_jobs:
        export_to_csv(new_jobs + updated_jobs)
    
    send_email_notification(new_jobs, updated_jobs)
    
    # Générer le dashboard
    print(f"[{get_current_datetime()}] Génération du dashboard...")
    subprocess.run(["python3", "generate_dashboard.py"])
    
    # Upload vers FTP
    if config["ftp_settings"].get("enabled", False):
        print(f"[{get_current_datetime()}] Upload vers FTP...")
        subprocess.run(["python3", "upload_to_ftp.py"])
    
    print(f"[{get_current_datetime()}] Tâche terminée")

if __name__ == "__main__":
    main()
"""

---

## 📌 **Instructions d'installation et d'utilisation**

### 🔧 **Étape 1 : Préparation sur ton serveur**
1. **Crée un dossier** sur ton serveur FTP :ftp.cluster129.hosting.ovh.net

