import uuid
from functools import wraps
from django.db import connection, transaction
from django.utils import timezone
from huey.exceptions import TaskLockedException

from archivebox.config import CONSTANTS

class SqliteSemaphore:
    def __init__(self, db_path, table_name, name, value=1, timeout=None):
        self.db_path = db_path
        self.table_name = table_name
        self.name = name
        self.value = value
        self.timeout = timeout or 86400  # Set a max age for lock holders

        # Ensure the table exists
        with connection.cursor() as cursor:
            cursor.execute(f"""
                CREATE TABLE IF NOT EXISTS {self.table_name} (
                    id TEXT PRIMARY KEY,
                    name TEXT,
                    timestamp DATETIME
                )
            """)

    def acquire(self, name=None):
        name = name or str(uuid.uuid4())
        now = timezone.now()
        expiration = now - timezone.timedelta(seconds=self.timeout)

        with transaction.atomic():
            # Remove expired locks
            with connection.cursor() as cursor:
                cursor.execute(f"""
                    DELETE FROM {self.table_name}
                    WHERE name = %s AND timestamp < %s
                """, [self.name, expiration])

            # Try to acquire the lock
            with connection.cursor() as cursor:
                cursor.execute(f"""
                    INSERT INTO {self.table_name} (id, name, timestamp)
                    SELECT %s, %s, %s
                    WHERE (
                        SELECT COUNT(*) FROM {self.table_name}
                        WHERE name = %s
                    ) < %s
                """, [name, self.name, now, self.name, self.value])

                if cursor.rowcount > 0:
                    return name

        # If we couldn't acquire the lock, remove our attempted entry
        with connection.cursor() as cursor:
            cursor.execute(f"""
                DELETE FROM {self.table_name}
                WHERE id = %s AND name = %s
            """, [name, self.name])

        return None

    def release(self, name):
        with connection.cursor() as cursor:
            cursor.execute(f"""
                DELETE FROM {self.table_name}
                WHERE id = %s AND name = %s
            """, [name, self.name])
        return cursor.rowcount > 0


LOCKS_DB_PATH = CONSTANTS.DATABASE_FILE.parent / 'locks.sqlite3'


def lock_task_semaphore(db_path, table_name, lock_name, value=1, timeout=None):
    """
    Lock which can be acquired multiple times (default = 1).

    NOTE: no provisions are made for blocking, waiting, or notifying. This is
    just a lock which can be acquired a configurable number of times.

    Example:

    # Allow up to 3 workers to run this task concurrently. If the task is
    # locked, retry up to 2 times with a delay of 60s.
    @huey.task(retries=2, retry_delay=60)
    @lock_task_semaphore('path/to/db.sqlite3', 'semaphore_locks', 'my-lock', 3)
    def my_task():
        ...
    """
    sem = SqliteSemaphore(db_path, table_name, lock_name, value, timeout)
    def decorator(fn):
        @wraps(fn)
        def inner(*args, **kwargs):
            tid = sem.acquire()
            if tid is None:
                raise TaskLockedException(f'unable to acquire lock {lock_name}')
            try:
                return fn(*args, **kwargs)
            finally:
                sem.release(tid)
        return inner
    return decorator
