import sqlite3
import datetime
import os
import logging
from flask import Flask, request, jsonify

# --- Configuration ---
DATABASE_FILE = 'weekly_counts.db'
# How many pings allowed per IP in the rate limit window
MAX_PINGS_PER_IP_WINDOW = 10
# The window duration for rate limiting (in seconds)
RATE_LIMIT_WINDOW_SECONDS = 3600 # 1 hour
# How old IP log entries should be before cleanup (in seconds)
IP_LOG_CLEANUP_AGE_SECONDS = 86400 # 24 hours

# --- Logging Setup ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# --- Flask App Initialization ---
app = Flask(__name__)

# --- Database Setup ---
def get_db():
    """Opens a new database connection if there is none yet for the current application context."""
    conn = sqlite3.connect(DATABASE_FILE)
    # Use TEXT type for dates to store in ISO format YYYY-MM-DD
    conn.execute("PRAGMA foreign_keys = ON") # Recommended for SQLite
    conn.row_factory = sqlite3.Row # Return rows as dictionary-like objects
    return conn

def init_db():
    """Initializes the database schema."""
    db = get_db()
    cursor = db.cursor()
    # Create weekly_pings table: Stores the unique pings received each week
    # IP address is *removed* from this table.
    # Added week_start_date (Monday) and week_end_date (Sunday).
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS weekly_pings (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            ping_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
            week_year INTEGER NOT NULL,           -- e.g., 2025
            week_number INTEGER NOT NULL,         -- ISO week number (1-53)
            week_start_date TEXT NOT NULL,      -- Date of Monday for this week (YYYY-MM-DD)
            week_end_date TEXT NOT NULL,        -- Date of Sunday for this week (YYYY-MM-DD)
            client_uuid TEXT NOT NULL,
            variant_id TEXT,
            update_id TEXT,
            UNIQUE(week_year, week_number, client_uuid) -- Ensure only one ping per UUID per week
        );
    ''')
    # Create ip_log table: Stores recent IP addresses for rate limiting/abuse detection (remains separate)
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS ip_log (
            ip_address TEXT PRIMARY KEY,
            ping_count INTEGER DEFAULT 1,
            window_start_timestamp DATETIME NOT NULL
        );
    ''')
    # Index for faster IP lookups
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_ip_log_window_start ON ip_log (window_start_timestamp);')
    # Index for faster weekly ping lookups
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_weekly_pings_lookup ON weekly_pings (week_year, week_number, client_uuid);')
    # Index for date lookups
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_weekly_pings_dates ON weekly_pings (week_start_date, week_end_date);')

    db.commit()
    db.close()
    logging.info("Database initialized successfully (schema updated for week dates, IP removed from weekly_pings).")

def cleanup_old_ip_logs():
    """Removes old entries from the ip_log table."""
    try:
        db = get_db()
        cutoff_time = datetime.datetime.now() - datetime.timedelta(seconds=IP_LOG_CLEANUP_AGE_SECONDS)
        cursor = db.cursor()
        # Use ISO format for comparison
        cursor.execute("DELETE FROM ip_log WHERE window_start_timestamp < ?", (cutoff_time.isoformat(),))
        deleted_count = cursor.rowcount
        db.commit()
        db.close()
        if deleted_count > 0:
            logging.info(f"Cleaned up {deleted_count} old IP log entries.")
    except Exception as e:
        logging.error(f"Error during IP log cleanup: {e}")

# --- Rate Limiting Logic (Unchanged) ---
def check_rate_limit(ip_address):
    """
    Checks if the IP address has exceeded the rate limit using the separate ip_log table.
    Returns True if allowed, False if rate limited.
    Updates the ip_log table accordingly.
    """
    cleanup_old_ip_logs() # Perform cleanup periodically on requests
    db = get_db()
    cursor = db.cursor()
    now = datetime.datetime.now()
    window_start = now - datetime.timedelta(seconds=RATE_LIMIT_WINDOW_SECONDS)

    cursor.execute("SELECT ip_address, ping_count, window_start_timestamp FROM ip_log WHERE ip_address = ?", (ip_address,))
    ip_entry = cursor.fetchone()

    if ip_entry:
        try:
            # Ensure timestamp from DB is parsed correctly
            entry_window_start = datetime.datetime.fromisoformat(ip_entry['window_start_timestamp'])
        except (ValueError, TypeError) as e:
             logging.error(f"Error parsing timestamp from ip_log for IP {ip_address}: {ip_entry['window_start_timestamp']}. Error: {e}. Resetting entry.")
             # Handle potentially corrupt data by resetting the entry
             cursor.execute("UPDATE ip_log SET ping_count = 1, window_start_timestamp = ? WHERE ip_address = ?", (now.isoformat(), ip_address))
             db.commit()
             db.close()
             return True # Allow this request but log error

        # Check if the existing entry is within the current rate limit window
        if entry_window_start >= window_start:
            if ip_entry['ping_count'] >= MAX_PINGS_PER_IP_WINDOW:
                logging.warning(f"Rate limit exceeded for IP: {ip_address}")
                db.close()
                return False # Rate limited
            else:
                # Increment count within the window
                cursor.execute("UPDATE ip_log SET ping_count = ping_count + 1 WHERE ip_address = ?", (ip_address,))
                db.commit()
                db.close()
                return True # Allowed
        else:
            # Entry is outside the current window, reset count and timestamp
            cursor.execute("UPDATE ip_log SET ping_count = 1, window_start_timestamp = ? WHERE ip_address = ?", (now.isoformat(), ip_address))
            db.commit()
            db.close()
            return True # Allowed
    else:
        # New IP address, insert it
        cursor.execute("INSERT INTO ip_log (ip_address, ping_count, window_start_timestamp) VALUES (?, 1, ?)", (ip_address, now.isoformat()))
        db.commit()
        db.close()
        return True # Allowed

# --- API Endpoint ---
@app.route('/ping', methods=['POST'])
def receive_ping():
    """Receives a ping from a client."""
    # Get client IP for rate limiting *only*
    client_ip = request.remote_addr
    if not client_ip:
        logging.warning("Could not determine client IP address for rate limiting.")
        # Decide if you want to allow pings without IP (potential abuse vector)
        # For now, we'll reject them.
        return jsonify({"status": "error", "message": "Could not determine client IP."}), 400

    # --- Apply Rate Limiting (uses separate ip_log table) ---
    if not check_rate_limit(client_ip):
        return jsonify({"status": "error", "message": "Rate limit exceeded."}), 429 # Too Many Requests

    # --- Get Data ---
    data = request.get_json()
    if not data:
        logging.warning(f"Received invalid/empty JSON data from {client_ip}.")
        return jsonify({"status": "error", "message": "Invalid JSON data."}), 400

    client_uuid = data.get('uuid')
    variant_id = data.get('variant_id')
    update_id = data.get('update_id')

    if not client_uuid:
        logging.warning(f"Missing 'uuid' in request from {client_ip}.")
        return jsonify({"status": "error", "message": "Missing 'uuid'."}), 400

    # --- Process Ping ---
    now = datetime.datetime.now()
    ping_timestamp_iso = now.isoformat() # Store timestamp in standard format

    # Use ISO week date standard (Year, Week Number, Day)
    iso_week = now.isocalendar()
    week_year = iso_week.year
    week_number = iso_week.week

    # Calculate Monday (day 1) and Sunday (day 7) of the ISO week
    try:
        week_start_date = datetime.date.fromisocalendar(week_year, week_number, 1)
        week_end_date = datetime.date.fromisocalendar(week_year, week_number, 7)
        week_start_date_iso = week_start_date.isoformat()
        week_end_date_iso = week_end_date.isoformat()
    except ValueError as e:
        logging.error(f"Error calculating week start/end dates for {week_year}-W{week_number}: {e}")
        return jsonify({"status": "error", "message": "Internal error calculating week dates."}), 500


    try:
        db = get_db()
        cursor = db.cursor()

        # Check if this UUID has already pinged this week
        cursor.execute("""
            SELECT id FROM weekly_pings
            WHERE week_year = ? AND week_number = ? AND client_uuid = ?
        """, (week_year, week_number, client_uuid))
        existing_ping = cursor.fetchone()

        if existing_ping:
            # Already counted this week, do nothing but acknowledge
            logging.debug(f"Ignoring duplicate ping for UUID {client_uuid} in week {week_year}-W{week_number}.")
            db.close()
            return jsonify({"status": "success", "message": "Already counted this week."}), 200
        else:
            # Insert new ping record - NO IP ADDRESS HERE
            cursor.execute("""
                INSERT INTO weekly_pings (
                    ping_timestamp, week_year, week_number, week_start_date, week_end_date,
                    client_uuid, variant_id, update_id
                )
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            """, (
                ping_timestamp_iso, week_year, week_number, week_start_date_iso, week_end_date_iso,
                client_uuid, variant_id, update_id
            ))
            db.commit()
            # Log includes IP for debugging/correlation if needed, but it's NOT stored in weekly_pings
            logging.info(f"Recorded ping from UUID {client_uuid} (Variant: {variant_id}, Update: {update_id}, IP: {client_ip}) for week {week_year}-W{week_number} ({week_start_date_iso} to {week_end_date_iso}).")
            db.close()
            return jsonify({"status": "success", "message": "Ping recorded."}), 201 # Created

    except sqlite3.Error as e:
        logging.error(f"Database error processing ping from {client_ip} (UUID: {client_uuid}): {e}")
        # Avoid leaking detailed DB errors to the client
        return jsonify({"status": "error", "message": "Database error processing ping."}), 500
    except Exception as e:
        logging.error(f"Unexpected error processing ping from {client_ip} (UUID: {client_uuid}): {e}")
        return jsonify({"status": "error", "message": "Internal server error."}), 500

@app.route('/', methods=['GET'])
def health_check():
    # Updated message slightly
    return jsonify({"status": "ok", "message": "stillCounter Weekly Counter server is running."}), 200


if __name__ == '__main__':
    # Ensure database exists and schema is initialized before starting app
    # Running init_db() every time ensures schema updates are applied if the code changes
    logging.info("Initializing database schema...")
    init_db()

    # Run the Flask app (for development only)
    # For production, use a WSGI server like Gunicorn or uWSGI
    # Example: gunicorn --bind 0.0.0.0:5000 server:app
    logging.info("Starting Flask development server...")
    app.run(debug=False, host='0.0.0.0', port=5000) # Set debug=False for production
