import json
import logging
from datetime import datetime, timedelta

from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import F
from django.utils import timezone

from djinsight.conf import djinsight_settings
from djinsight.models import PageViewEvent, PageViewStatistics, PageViewSummary

logger = logging.getLogger(__name__)

_redis_client = None


def _get_redis_client():
    """Lazy initialization of Redis client. Returns None if Redis unavailable."""
    global _redis_client
    if _redis_client is None:
        try:
            from djinsight.providers.redis import RedisProvider
            provider = RedisProvider()
            _redis_client = provider.client
        except Exception:
            logger.warning("Redis client not available, tasks requiring Redis will be skipped")
            return None
    return _redis_client

# Try to import Celery - if not available, tasks will be regular functions
try:
    from celery import shared_task

    HAS_CELERY = True
except ImportError:
    # Fallback decorator - must handle bind=True kwargs gracefully
    def shared_task(*args, **kwargs):
        def decorator(func):
            return func
        if args and callable(args[0]):
            return args[0]
        return decorator

    HAS_CELERY = False


@shared_task(
    bind=True,
    max_retries=3,
    default_retry_delay=60,
    task_time_limit=djinsight_settings.PROCESS_TASK_TIME_LIMIT,
    task_soft_time_limit=djinsight_settings.PROCESS_TASK_SOFT_TIME_LIMIT,
)
def process_page_views_task(
    self,
    batch_size=None,
    max_records=None,
):
    """
    Celery task to process page views from Redis and store them in the database.

    Args:
        batch_size (int): Number of records to process in a single transaction
        max_records (int): Maximum number of records to process in a single run

    Returns:
        int: Number of records processed
    """
    try:
        batch_size = batch_size or djinsight_settings.PROCESS_BATCH_SIZE
        max_records = max_records or djinsight_settings.PROCESS_MAX_RECORDS
        return process_page_views(batch_size, max_records)
    except Exception as exc:
        logger.error(f"Error processing page views: {exc}")
        if HAS_CELERY:
            raise self.retry(exc=exc)
        else:
            raise


@shared_task(
    bind=True,
    max_retries=3,
    default_retry_delay=60,
    task_time_limit=djinsight_settings.SUMMARY_TASK_TIME_LIMIT,
    task_soft_time_limit=djinsight_settings.SUMMARY_TASK_SOFT_TIME_LIMIT,
)
def generate_daily_summaries_task(
    self, days_back=None
):
    """
    Celery task to generate daily page view summaries.

    Args:
        days_back (int): Number of days back to process

    Returns:
        int: Number of summaries generated
    """
    try:
        days_back = days_back or djinsight_settings.SUMMARY_DAYS_BACK
        return generate_daily_summaries(days_back)
    except Exception as exc:
        logger.error(f"Error generating daily summaries: {exc}")
        if HAS_CELERY:
            raise self.retry(exc=exc)
        else:
            raise


@shared_task(
    bind=True,
    max_retries=3,
    default_retry_delay=60,
    task_time_limit=djinsight_settings.CLEANUP_TASK_TIME_LIMIT,
    task_soft_time_limit=djinsight_settings.CLEANUP_TASK_SOFT_TIME_LIMIT,
)
def cleanup_old_data_task(
    self, days_to_keep=None
):
    """
    Celery task to cleanup old page view logs.

    Args:
        days_to_keep (int): Number of days of logs to keep

    Returns:
        int: Number of records deleted
    """
    try:
        days_to_keep = days_to_keep or djinsight_settings.CLEANUP_DAYS_TO_KEEP
        return cleanup_old_data(days_to_keep)
    except Exception as exc:
        logger.error(f"Error cleaning up old data: {exc}")
        if HAS_CELERY:
            raise self.retry(exc=exc)
        else:
            raise


def process_page_views(
    batch_size=None,
    max_records=None,
):
    """
    Process page views from Redis and store them in the database.

    Args:
        batch_size (int): Number of records to process in a single transaction
        max_records (int): Maximum number of records to process in a single run

    Returns:
        int: Number of records processed
    """
    batch_size = batch_size or djinsight_settings.PROCESS_BATCH_SIZE
    max_records = max_records or djinsight_settings.PROCESS_MAX_RECORDS
    redis_client = _get_redis_client()
    if not redis_client:
        logger.error("Redis client not available")
        return 0

    logger.info("Starting to process page views from Redis")

    try:
        # Get all keys matching the page view pattern, excluding counters and sessions
        prefix = djinsight_settings.REDIS_KEY_PREFIX
        pattern = f"{prefix}*"
        exclude_patterns = [
            f"{prefix}counter:",
            f"{prefix}unique_counter:",
            f"{prefix}session:",
        ]

        # Use SCAN instead of KEYS to avoid blocking Redis
        keys = []
        for key in redis_client.scan_iter(match=pattern, count=1000):
            decoded = key.decode("utf-8")
            if not any(decoded.startswith(exclude) for exclude in exclude_patterns):
                keys.append(decoded)
                if len(keys) >= max_records:
                    break

        if not keys:
            logger.info("No page views to process")
            return 0

        logger.info(f"Found {len(keys)} page views to process")

        # Process in batches
        processed_count = 0
        for i in range(0, len(keys), batch_size):
            batch_keys = keys[i : i + batch_size]
            batch_processed = process_batch(batch_keys)
            processed_count += batch_processed

            # Log progress
            if i % (batch_size * 10) == 0:
                logger.info(f"Processed {processed_count} / {len(keys)} page views")

        logger.info(f"Completed processing {processed_count} page views")
        return processed_count

    except Exception as e:
        logger.error(f"Error processing page views: {e}")
        raise


def process_batch(keys):
    """
    Process a batch of page views.

    Args:
        keys (list): List of Redis keys to process

    Returns:
        int: Number of records processed in this batch
    """
    redis_client = _get_redis_client()
    if not redis_client:
        return 0

    pipe = redis_client.pipeline()
    for key in keys:
        pipe.get(key)
    values = pipe.execute()

    page_view_events = []
    page_view_counters = {}
    processed_count = 0

    for key, value in zip(keys, values):
        if value is None:
            continue

        try:
            data = json.loads(value.decode("utf-8"))

            # Extract data with validation
            page_id = data.get("object_id")
            content_type = data.get("content_type")
            url = data.get("url")
            session_key = data.get("session_key")
            ip_address = data.get("ip_address")
            user_agent = data.get("user_agent")
            referrer = data.get("referrer")
            timestamp_value = data.get("timestamp")
            is_unique = data.get("is_unique", False)

            # Skip if missing essential data
            if not all([page_id, content_type, url]):
                logger.warning(f"Skipping incomplete page view data in key {key}")
                continue

            # Convert timestamp
            if timestamp_value:
                if isinstance(timestamp_value, str):
                    timestamp = datetime.fromtimestamp(
                        int(timestamp_value), tz=timezone.get_current_timezone()
                    )
                else:
                    timestamp = datetime.fromtimestamp(
                        timestamp_value, tz=timezone.get_current_timezone()
                    )
            else:
                timestamp = timezone.now()

            app_label, model = content_type.split(".")
            ct = ContentType.objects.get_by_natural_key(app_label, model)

            page_view_events.append(
                PageViewEvent(
                    content_type=ct,
                    object_id=page_id,
                    url=url,
                    session_key=session_key[:255] if session_key else "",
                    ip_address=ip_address,
                    user_agent=user_agent[:1000] if user_agent else "",
                    referrer=referrer[:500] if referrer else "",
                    timestamp=timestamp,
                    is_unique=is_unique,
                )
            )

            counter_key = (ct.id, page_id)
            if counter_key not in page_view_counters:
                page_view_counters[counter_key] = (1, 1 if is_unique else 0)
            else:
                total, unique = page_view_counters[counter_key]
                page_view_counters[counter_key] = (
                    total + 1,
                    unique + (1 if is_unique else 0),
                )

            processed_count += 1

        except (json.JSONDecodeError, ValueError, TypeError) as e:
            logger.error(f"Error processing page view {key}: {e}")
            continue
        except Exception as e:
            logger.error(f"Unexpected error processing page view {key}: {e}")
            continue

    if page_view_events or page_view_counters:
        with transaction.atomic():
            if page_view_events:
                PageViewEvent.objects.bulk_create(page_view_events, batch_size=500)

            for (content_type_id, object_id), (total, unique) in page_view_counters.items():
                try:
                    stats, created = PageViewStatistics.objects.get_or_create(
                        content_type_id=content_type_id,
                        object_id=object_id,
                    )
                    now = timezone.now()
                    updates = {
                        "total_views": F("total_views") + total,
                        "unique_views": F("unique_views") + unique,
                        "last_viewed_at": now,
                    }
                    if not stats.first_viewed_at:
                        updates["first_viewed_at"] = now
                    PageViewStatistics.objects.filter(pk=stats.pk).update(**updates)

                except Exception as e:
                    logger.error(
                        f"Error updating page statistics for content_type {content_type_id} object {object_id}: {e}"
                    )

    # Delete processed keys from Redis
    if keys:
        try:
            redis_client.delete(*keys)
        except Exception as e:
            logger.error(f"Error deleting processed keys from Redis: {e}")

    return processed_count


def generate_daily_summaries(days_back=None):
    """
    Generate daily page view summaries from detailed logs.

    Args:
        days_back (int): Number of days back to process

    Returns:
        int: Number of summaries generated
    """
    days_back = days_back or djinsight_settings.SUMMARY_DAYS_BACK
    logger.info(f"Generating daily summaries for the last {days_back} days")

    end_date = timezone.now().date()
    start_date = end_date - timedelta(days=days_back)

    summaries_created = 0

    page_views = (
        PageViewEvent.objects.filter(
            timestamp__date__gte=start_date, timestamp__date__lte=end_date
        )
        .values("content_type", "object_id", "timestamp__date")
        .distinct()
    )

    for view_data in page_views:
        content_type_id = view_data["content_type"]
        content_type = ContentType.objects.get(pk=content_type_id)
        object_id = view_data["object_id"]
        date = view_data["timestamp__date"]

        date_views = PageViewEvent.objects.filter(
            content_type=content_type,
            object_id=object_id,
            timestamp__date=date
        )

        total_views = date_views.count()
        unique_views = date_views.values("session_key").distinct().count()

        summary, created = PageViewSummary.objects.update_or_create(
            content_type=content_type,
            object_id=object_id,
            date=date,
            defaults={
                "total_views": total_views,
                "unique_views": unique_views,
            },
        )

        if created:
            summaries_created += 1

    logger.info(f"Generated {summaries_created} daily summaries")
    return summaries_created


def cleanup_old_data(days_to_keep=None):
    """
    Cleanup old page view logs older than specified days.

    Args:
        days_to_keep (int): Number of days of logs to keep

    Returns:
        int: Number of records deleted
    """
    days_to_keep = days_to_keep or djinsight_settings.CLEANUP_DAYS_TO_KEEP
    cutoff_date = timezone.now() - timedelta(days=days_to_keep)

    logger.info(f"Cleaning up page view logs older than {cutoff_date}")

    # Delete in batches to avoid long-running transactions
    batch_size = djinsight_settings.CLEANUP_BATCH_SIZE
    deleted_count = 0
    while True:
        batch_ids = list(
            PageViewEvent.objects.filter(timestamp__lt=cutoff_date)
            .values_list("id", flat=True)[:batch_size]
        )
        if not batch_ids:
            break
        count, _ = PageViewEvent.objects.filter(id__in=batch_ids).delete()
        deleted_count += count

    logger.info(f"Deleted {deleted_count} old page view events")

    # Also cleanup old Redis session keys (this is optional)
    redis_client = _get_redis_client()
    if redis_client:
        try:
            # Clean up session keys older than days_to_keep
            prefix = djinsight_settings.REDIS_KEY_PREFIX
            session_pattern = f"{prefix}session:*"
            session_keys = list(redis_client.scan_iter(match=session_pattern, count=1000))

            # Check TTL and delete expired keys manually
            # (Redis should handle this automatically, but just in case)
            deleted_sessions = 0
            for key in session_keys:
                ttl = redis_client.ttl(key)
                if ttl == -1:  # Key exists but has no expiration
                    redis_client.delete(key)
                    deleted_sessions += 1

            if deleted_sessions > 0:
                logger.info(
                    f"Cleaned up {deleted_sessions} orphaned session keys from Redis"
                )

        except Exception as e:
            logger.error(f"Error cleaning up Redis session keys: {e}")

    return deleted_count


def run_process_page_views(verbosity=1, **options):
    """Function that can be called from management command"""
    batch_size = options.get("batch_size") or djinsight_settings.PROCESS_BATCH_SIZE
    max_records = options.get("max_records") or djinsight_settings.PROCESS_MAX_RECORDS

    if verbosity >= 1:
        print(
            f"Processing page views with batch_size={batch_size}, max_records={max_records}"
        )

    processed = process_page_views(batch_size, max_records)

    if verbosity >= 1:
        print(f"Processed {processed} page views")

    return processed


def run_generate_summaries(verbosity=1, **options):
    """Function that can be called from management command"""
    days_back = options.get("days_back") or djinsight_settings.SUMMARY_DAYS_BACK

    if verbosity >= 1:
        print(f"Generating daily summaries for the last {days_back} days")

    generated = generate_daily_summaries(days_back)

    if verbosity >= 1:
        print(f"Generated {generated} daily summaries")

    return generated


def run_cleanup_old_data(verbosity=1, **options):
    """Function that can be called from management command"""
    days_to_keep = options.get("days_to_keep") or djinsight_settings.CLEANUP_DAYS_TO_KEEP

    if verbosity >= 1:
        print(f"Cleaning up data older than {days_to_keep} days")

    deleted = cleanup_old_data(days_to_keep)

    if verbosity >= 1:
        print(f"Deleted {deleted} old records")

    return deleted
