import json
import socket
import threading
import time
from flask import Flask, request, jsonify
from flask_cors import CORS
import os
# --- Configuration ---
BARCODES_FILE = 'barcodes.json'
SERVER_PORT = 5678  # Port for the web server (for Tampermonkey)
BROADCAST_PORT = 5679 # Port for network discovery and sync
BROADCAST_INTERVAL = 20 # Seconds between sync broadcasts
# --- Flask Web Server Setup ---
app = Flask(__name__)
CORS(app) # Allow requests from the Tampermonkey script
# --- Barcode Data Management ---
data_lock = threading.Lock()
def load_barcodes():
    """Safely loads barcodes from the JSON file."""
    with data_lock:
        if not os.path.exists(BARCODES_FILE):
            return []
        try:
            with open(BARCODES_FILE, 'r') as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError):
            return []
def save_barcodes(barcodes):
    """Safely saves barcodes to the JSON file."""
    with data_lock:
        # Sort by count to maintain some order
        barcodes.sort(key=lambda x: x.get('count', 0))
        with open(BARCODES_FILE, 'w') as f:
            json.dump(barcodes, f, indent=4)
def merge_barcodes(local_list, received_list):
    """Merges two lists of barcodes, avoiding duplicates based on the 'barcode' value."""
    merged_dict = {item['barcode']: item for item in local_list}
    changed = False
    for item in received_list:
        if item['barcode'] not in merged_dict:
            merged_dict[item['barcode']] = item
            changed = True
        else:
            if item.get('found', False) and not merged_dict[item['barcode']].get('found', False):
                merged_dict[item['barcode']]['found'] = True
                changed = True
    if not changed:
        return local_list # Return original list if no changes
    final_list = list(merged_dict.values())
    for i, item in enumerate(final_list):
        item['count'] = i + 1
    return final_list
# --- API Endpoints for Tampermonkey ---
@app.route('/get_barcodes', methods=['GET'])
def get_barcodes():
    """Endpoint for the script to fetch the current list of barcodes."""
    return jsonify(load_barcodes())
# *** NEW, SAFER ENDPOINT FOR ADDING BARCODES ***
@app.route('/add_barcode', methods=['POST'])
def add_barcode():
    """Endpoint for the script to send a single new barcode."""
    new_entry = request.json
    if not new_entry or 'barcode' not in new_entry:
        return jsonify({"status": "error", "message": "Invalid data format."}), 400
    barcodes = load_barcodes()
    # Check for duplicates
    if any(b['barcode'] == new_entry['barcode'] for b in barcodes):
        return jsonify({"status": "success", "message": "Barcode already exists."}), 200
    # Add the new entry and re-assign counts
    barcodes.append(new_entry)
    for i, item in enumerate(barcodes):
        item['count'] = i + 1
    save_barcodes(barcodes)
    print(f"[{time.ctime()}] Added new barcode: {new_entry['barcode']}")
    return jsonify({"status": "success", "message": "Barcode added."}), 201
@app.route('/update_barcodes', methods=['POST'])
def update_barcodes():
    """Endpoint for the script to send a full list of barcodes (e.g., after deleting)."""
    new_barcodes = request.json
    if isinstance(new_barcodes, list):
        save_barcodes(new_barcodes)
        return jsonify({"status": "success", "message": "Barcode list updated."}), 200
    return jsonify({"status": "error", "message": "Invalid data format."}), 400
# --- Network Syncing Logic (UDP Broadcast) ---
def broadcast_sync():
    """Periodically sends the entire local barcode list to the network."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    while True:
        time.sleep(BROADCAST_INTERVAL)
        barcodes = load_barcodes()
        if barcodes:
            message = json.dumps(barcodes).encode('utf-8')
            try:
                sock.sendto(message, ('', BROADCAST_PORT))
                # print(f"[{time.ctime()}] Sent sync broadcast with {len(barcodes)} items.")
            except Exception as e:
                print(f"Error sending broadcast: {e}")
def listen_for_sync():
    """Listens for barcode lists from other computers and merges them."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('', BROADCAST_PORT))
    print(f"Listening for sync data on UDP port {BROADCAST_PORT}")
    my_ip = socket.gethostbyname(socket.gethostname())
    while True:
        try:
            data, addr = sock.recvfrom(65507)
            if addr[0] != my_ip:
                received_barcodes = json.loads(data.decode('utf-8'))
                local_barcodes = load_barcodes()
                merged_list = merge_barcodes(local_barcodes, received_barcodes)
                if merged_list is not local_barcodes:
                    save_barcodes(merged_list)
                    print(f"[{time.ctime()}] Received and merged {len(received_barcodes)} barcodes from {addr[0]}. New total: {len(merged_list)}.")
        except Exception as e:
            print(f"Error receiving sync data: {e}")
if __name__ == '__main__':
    print("--- Barcode Sync Server ---")
    if not os.path.exists(BARCODES_FILE):
        save_barcodes([])
        print(f"Created empty barcodes file at: {os.path.abspath(BARCODES_FILE)}")
    listener_thread = threading.Thread(target=listen_for_sync, daemon=True)
    listener_thread.start()
    broadcaster_thread = threading.Thread(target=broadcast_sync, daemon=True)
    broadcaster_thread.start()
    print(f"Starting web server for Tampermonkey on http://localhost:{SERVER_PORT}")
    app.run(host='0.0.0.0', port=SERVER_PORT)