"""
ActivityPushMixin — push/sync loop, DB-to-server upload, and data cleanup.

Provides the recurring upload loop, SQLite-to-S3 backup, and stale-data purge for
:class:`~ws_package.controllers.activity_controller.ActivityController`.
All methods assume ``self`` is an ``ActivityController`` instance and have access
to all attributes defined in ``ActivityController.__init__``.

Thread-safety:
    All public methods are async coroutines and must be awaited from the async
    monitoring loop.  They must not be called concurrently from multiple coroutines
    without external coordination.
"""
from __future__ import annotations

import asyncio
import datetime
import os
import platform
import sys
import time
from pathlib import Path
from typing import Optional

from sqlalchemy import select, delete

from ws_package.log.writer import LogWriter
from ws_package.entities.user_entity import UserEntity
from ws_package.exceptions.custom_exceptions import ValidationError, NetworkError, ScreenshotError
from ws_package.utils.system import get_ip, get_os_version, precise_sleep

logger = LogWriter.file(__name__)


class ActivityPushMixin:
    """Push-loop orchestration, database-to-S3 sync, and stale-data cleanup for ActivityController."""

    async def start_push_activity(self, user_entity: UserEntity, stop_event=None):
        """Start the push-activity loop: purge 7-day-old intervals then begin recurring upload.

        Args:
            user_entity: Authenticated user entity used for API calls.
            stop_event: Optional asyncio event; when set the loop exits cleanly.
        """
        await self.offline7days()
        await precise_sleep(5)
        await self._push_activity_log(user_entity, stop_event)

    async def timezone_wrong(self):
        """Return the timezone flag from the most recent Appmonitor entry (0 = correct, 1 = wrong)."""
        # Database operation timeout constant
        DB_OPERATION_TIMEOUT = 5.0

        from ws_package.models.base import async_session, Appmonitor
        from sqlalchemy import select
        from ws_package.utils.db_lock_manager import db_lock_manager

        with db_lock_manager.read_lock():
            async with async_session() as session:
                query = (
                    select(Appmonitor)
                    .order_by(Appmonitor.timestamp.desc())
                    .limit(1)
                )
                try:
                    result = await asyncio.wait_for(
                        session.execute(query),
                        timeout=DB_OPERATION_TIMEOUT
                    )
                    last_entry = result.scalar_one_or_none()

                    if last_entry:
                        return last_entry.timezone
                    else:
                        return 0
                except asyncio.TimeoutError:
                    logger.error("Database query timed out after %ss in timezone_wrong", DB_OPERATION_TIMEOUT)
                    return 0
                except Exception as e:
                    logger.error("Error getting timezone: %s", e)
                    return 0

    async def truncate_file(self, file_path, user_entity: UserEntity):
        """Truncate file with proper resource management."""
        logger.info("in truncate file")

        # Use context managers for all file operations
        try:
            prev_date = datetime.datetime.now() - datetime.timedelta(hours=1)
            prev_date = prev_date.strftime("%Y-%m-%d-%H")

            if getattr(sys, 'frozen', False):
                script_path = Path(sys.executable).resolve()
                target_directory = script_path.parents[0]
            else:
                script_path = Path(__file__).resolve()
                target_directory = f"{script_path.parents[3]}/workstatus"

            new_file = str(Path(target_directory) / "logsy" / f"{prev_date}-logs.txt")

            # Ensure the logsy directory exists
            os.makedirs(os.path.dirname(new_file), exist_ok=True)

            # Use context managers for file operations with UTF-8 encoding
            try:
                # Try to read the file with multiple encoding strategies
                old_data = None
                encodings_to_try = ['utf-8', 'utf-8-sig', 'latin-1', 'cp1252']

                for encoding in encodings_to_try:
                    try:
                        with open(file_path, 'r', encoding=encoding, errors='replace') as f:
                            old_data = f.read()
                        logger.info("Successfully read file %s with encoding: %s", file_path, encoding)
                        break
                    except UnicodeDecodeError as e:
                        logger.warning("Failed to read %s with encoding %s: %s", file_path, encoding, e)
                        continue

                if old_data is None:
                    raise Exception(f"Could not read file {file_path} with any of the attempted encodings: {encodings_to_try}")

                # Write to new file
                with open(new_file, 'w', encoding='utf-8') as f:
                    f.write(old_data)

                # Truncate original file
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.truncate(0)

            except Exception as e:
                logger.error("Error in file truncation: %s", e, exc_info=True)
                return

            # Upload to server
            try:
                logger.info("Sending window Logs to server")
                task = asyncio.create_task(
                    self.screenshot_controller.screenshot_service.sync(
                        f'{new_file}',
                        f"{self.s3_logs_path}{user_entity.org_id}/{user_entity.user_id}/{prev_date}-logs.txt",
                        user_entity
                    ),
                )
                # Track task to prevent silent failures
                self._background_tasks.add(task)
                task.add_done_callback(self._background_tasks.discard)

                log_path = await task
                logger.info(log_path)
            except Exception as e:
                logger.error("Error uploading logs: %s", e, exc_info=True)

        except Exception as e:
            logger.error("Error in truncate_file: %s", e, exc_info=True)

    async def offline7days(self):
        """Mark activity intervals older than 7 days as uploaded so they are not re-sent.

        The cutoff count is derived from ``self.interval`` (10 min → 1008, 5 min → 2016,
        15 min → 672 intervals per 7-day window).
        """
        logger.info("checking for Offline 7 days case")
        if int(self.interval/60) == 15:
            seven_days_old = await self.activity_service.sevendays(672)  # there are 672 intervals in 7 days for 15 minutes interval
        elif int(self.interval/60) == 5:
            seven_days_old = await self.activity_service.sevendays(2016)  # there are 2016 intervals in 7 days for 5 minutes interval
        else:
            seven_days_old = await self.activity_service.sevendays(1008)  # there are 1008 intervals in 7 days for 10 minutes interval
        if not seven_days_old:
            logger.info("Offline 7 days return none")
            return
        for interval in seven_days_old:
            logger.info("Offline 7 days %s", interval)
            await self.activity_service.update_activity_interval(interval)

    async def delete_old_data_and_screenshots(self):
        """
        Delete old data and screenshots older than 7 days.

        FIXED: Separated file operations from database operations to prevent long lock holds.
        Pattern: Read from DB (quick) -> Release lock -> Delete files (slow) -> Re-acquire lock -> Delete from DB (quick)
        """
        # Database operation timeout constant
        DB_OPERATION_TIMEOUT = 5.0

        from ws_package.models.base import async_session, ActivityInterval, Screenshots, Appmonitor, OrgConfigurations
        from sqlalchemy import select, delete
        from ws_package.utils.db_lock_manager import db_lock_manager
        from ws_package import logs

        if getattr(sys, 'frozen', False) and platform.system() == "Linux":  # Running as a PyInstaller EXE in linux
            script_path = Path(sys.executable).resolve()
            target_directory = script_path.parents[0]  # Just one level up from the executable
            target_directory_stealth = script_path.parents[1]  # Two levels up from the executable
        elif getattr(sys, 'frozen', False) and platform.system() == "Windows":  # Running as a PyInstaller EXE in Windows
            script_path = Path(sys.executable).resolve()
            target_directoryw = script_path.parents[0]
        elif sys.platform == 'darwin':
            target_directory_mac = Path.home() / "Library/Application Support/Workstatus"
        else:  # DEV MODE
            script_path = Path(__file__).resolve()
            target_directory = f"{script_path.parents[3]}/workstatus"
            target_directory_stealth = f"{target_directory}/workstatus/"
        logger.info("Starting deletion of old data and screenshots.")

        try:
            seven_days_ago = datetime.datetime.now() - datetime.timedelta(days=7)

            # STEP 1: Get list of screenshots to delete (QUICK, inside lock)
            screenshots_to_delete = []
            with db_lock_manager.write_lock():
                async with async_session() as session:
                    screenshot_query = select(Screenshots).where(Screenshots.timestamp < seven_days_ago)
                    try:
                        result = await asyncio.wait_for(
                            session.execute(screenshot_query),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        old_screenshots = result.scalars().all()

                        # Extract screenshot data while session is still open
                        for screenshot in old_screenshots:
                            screenshots_to_delete.append({
                                'id': screenshot.id,
                                'file_location': screenshot.file_location,
                                'file_name': screenshot.file_name
                            })
                    except asyncio.TimeoutError:
                        logger.error("Database query timed out after %ss while getting old screenshots", DB_OPERATION_TIMEOUT)
                        return
                    except Exception as e:
                        logger.error("Error getting old screenshots: %s", e)
                        return

            # STEP 2: Delete files OUTSIDE lock (SLOW I/O operations - don't block database)
            if screenshots_to_delete:
                logger.info("Deleting %s screenshot files...", len(screenshots_to_delete))
                for idx, screenshot_data in enumerate(screenshots_to_delete):
                    file_location = screenshot_data['file_location']
                    file_name = screenshot_data['file_name']

                    try:
                        if platform.system() == "Linux":
                            try:
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("/")[1],
                                    f"{target_directory_stealth}/screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s/screenshots", e, target_directory_stealth)
                            try:
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("/")[1],
                                    f"{target_directory}/screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s/screenshots", e, target_directory)
                        elif platform.system() == "Windows":
                            try:
                                logger.info("Cleaning function Build for windows")
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("\\")[1],
                                    f"{target_directoryw}\\screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s\\screenshots", e, target_directoryw)
                            # Log cleanup (only once, not per screenshot)
                            if idx == 0:  # Only cleanup logs once (first iteration)
                                try:
                                    logs.cleanup_old_files(f"{target_directoryw}\\logsy")
                                except Exception as e:
                                    logger.error("Error in deleting logs: %s in %s\\logsy", e, target_directoryw)
                        elif sys.platform == 'darwin':
                            try:
                                self.screenshot_controller.delete_screenshot(
                                    file_location.split("/")[1],
                                    f"{target_directory_mac}/screenshots"
                                )
                            except Exception as e:
                                logger.error("Error deleting screenshot: %s in %s/screenshots", e, target_directory_mac)
                    except Exception as e:
                        logger.error("Error deleting screenshot file %s: %s", file_name, e)
                        # Continue with next screenshot even if one fails
            else:
                logger.info("No screenshots older than 7 days found for deletion.")

            # STEP 3: Delete database entries (QUICK, inside lock)
            with db_lock_manager.write_lock():
                async with async_session() as session:
                    try:
                        # Delete screenshots from database (bulk delete is faster)
                        if screenshots_to_delete:
                            screenshot_ids = [s['id'] for s in screenshots_to_delete]
                            await asyncio.wait_for(
                                session.execute(
                                    delete(Screenshots).where(Screenshots.id.in_(screenshot_ids))
                                ),
                                timeout=DB_OPERATION_TIMEOUT
                            )
                            logger.info("Deleted %s screenshot database entries.", len(screenshot_ids))

                        # Delete ActivityInterval entries older than 7 days
                        await asyncio.wait_for(
                            session.execute(delete(ActivityInterval).where(ActivityInterval.start_time < seven_days_ago)),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Deleted old ActivityInterval entries.")

                        # Delete OrgConfigurations entries older than 7 days
                        await asyncio.wait_for(
                            session.execute(delete(OrgConfigurations).where(OrgConfigurations.timestamp < seven_days_ago)),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Deleted old OrgConfigurations entries.")

                        # Delete Appmonitor entries older than 7 days
                        await asyncio.wait_for(
                            session.execute(delete(Appmonitor).where(Appmonitor.timestamp < seven_days_ago)),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Deleted old Appmonitor entries.")

                        # Commit with timeout
                        await asyncio.wait_for(
                            session.commit(),
                            timeout=DB_OPERATION_TIMEOUT
                        )
                        logger.info("Successfully deleted old data and screenshots from specified tables.")

                    except asyncio.TimeoutError:
                        logger.error("Database operation timed out after %ss in delete_old_data_and_screenshots", DB_OPERATION_TIMEOUT)
                        await session.rollback()
                    except Exception as e:
                        logger.error("Error deleting database entries: %s", e, exc_info=True)
                        await session.rollback()

        except Exception as e:
            logger.error("Error in deleting old data and screenshots: %s", e, exc_info=True)

    async def start_status_activity(self, user_entity: UserEntity, stop_event):
        """
        Status and maintenance activity loop.
        Sets up credentials and org info, then runs periodic db_to_server uploads and cleanup.
        """
        if user_entity is None:
            logger.error("start_status_activity called with None user_entity, cannot proceed")
            return

        await self.status_controller.set_cred(user_entity)
        # Set org_id from user_entity
        self.org_id = user_entity.org_id
        # Update org_id in ScreenshotController and its ScreenshotService
        self.screenshot_controller.org_id = self.org_id
        self.screenshot_controller.screenshot_service.org_id = self.org_id
        logger.info("org_id set to: %s for ScreenshotController and ScreenshotService", self.org_id)
        logger.info("Setting S3 details for db to server")

        # Status check is now only called on app restart and system wake, not in a loop
        # This thread just keeps running for db_to_server operations
        while not stop_event.is_set():
            try:
                try:
                    await self.db_to_server(user_entity)
                except Exception as e:
                    logger.error("Error in db_to_server: %s", e)

                # Sleep for a long time (only wake up for stop_event)
                # Status check will be triggered by wake callback or app restart
                total_sleep = 3600  # 1 hour
                chunk_duration = 600  # 10 minutes per chunk
                chunks = total_sleep // chunk_duration
                for _ in range(chunks):
                    if stop_event.is_set():
                        break
                    await precise_sleep(chunk_duration, stop_event)

                if stop_event.is_set():
                    break

                try:
                    await self.delete_old_data_and_screenshots()

                    # Perform periodic log cleanup (works offline, independent of server connectivity)
                    try:
                        from ws_package.utils.logs import perform_comprehensive_log_cleanup
                        from ws_package.configs import config
                        cleanup_enabled = config('log_cleanup_enabled', 'System', dtype='bool')
                        if cleanup_enabled is None or cleanup_enabled:
                            perform_comprehensive_log_cleanup()
                    except Exception as e:
                        logger.debug("Error during periodic log cleanup (non-fatal): %s", e)

                    if sys.platform == "darwin":
                        logger.info("Sending mac logs to server")
                        mac_log = Path.home() / "Library/Application Support/Workstatus/logs/workstatus_error.log"
                        if mac_log.exists():
                            timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
                            s3_key = f"{self.s3_logs_path}{user_entity.org_id}/{user_entity.user_id}/{timestamp}-logs.txt"

                            try:
                                task = asyncio.create_task(
                                    self.screenshot_controller.screenshot_service.sync(
                                        str(mac_log),
                                        s3_key,
                                        user_entity
                                    )
                                )
                                # Track task to prevent silent failures
                                self._background_tasks.add(task)
                                task.add_done_callback(self._background_tasks.discard)

                                logs_path = await task
                                logger.info("Running push logs")
                                logger.info("Uploaded from: %s", mac_log)
                                logger.info("Uploaded to: %s", logs_path)
                            except ValidationError as e:
                                logger.error("Screenshot sync validation error for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation
                            except ScreenshotError as e:
                                logger.error("Screenshot sync error for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation
                            except NetworkError as e:
                                logger.error("Network error during screenshot sync for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation
                            except Exception as e:
                                logger.error("Unexpected error during screenshot sync for Mac logs: %s", e)
                                logger.error("Context: file=%s, dest=%s", mac_log, s3_key)
                                # Continue processing - don't fail the entire operation

                            # Clear the log file after successful upload
                            mac_log.write_text("")
                            logger.info("Cleared log file: %s", mac_log)
                        else:
                            logger.info("Log file %s does not exist, skipping upload.", mac_log)
                    else:
                        logger.debug("MAC ErrorLog file does not exist, skipping upload.")
                except Exception as e:
                    logger.exception("Error in uploading MAC logs: %s", e)

            except Exception as e:
                logger.error("Error in status activity loop: %s", e, exc_info=True)
                # Continue the loop even if there's an error
                await asyncio.sleep(5)  # Brief pause before retrying
                continue

    async def db_to_server(self, user_entity: UserEntity):
        """Upload a safe copy of the SQLite database to S3 for backup/diagnostics."""
        logger.info("Running db to server")
        if getattr(sys, 'frozen', False) and platform.system() == "Linux":
            script_path = Path(sys.executable).resolve()
            target_directory = script_path.parents[0]
            database_dir = target_directory / "ws_monitor.db"
        elif getattr(sys, 'frozen', False) and platform.system() == "Windows":
            script_path = Path(sys.executable).resolve()
            target_directory = script_path.parents[0]
            database_dir = target_directory / "ws_monitor.db"
        else:
            database_dir = Path.home() / "Library/Application Support/Workstatus/ws_monitor.db"
        timestamp_db = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        dt_obj = datetime.datetime.strptime(timestamp_db, "%Y-%m-%d_%H-%M-%S")
        unix_timestamp = int(time.mktime(dt_obj.timetuple()))
        database_server = f"database/{user_entity.org_id}/{user_entity.user_id}/{unix_timestamp}.db"

        if not database_dir.exists():
            logger.error("Database file not found for upload: %s", database_dir)
            return

        try:
            # Use safe file copy that coordinates with database locks
            import tempfile
            import shutil
            from ws_package.utils.db_lock_manager import db_lock_manager

            temp_file = None

            # Create copy under database write lock to ensure consistency
            with db_lock_manager.write_lock(timeout=60.0):
                # Wait for any pending transactions
                await asyncio.sleep(0.2)  # Allow any pending operations to complete

                # Create temporary copy safely
                temp_file = None
                temp_file_path = None
                try:
                    temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.db')
                    temp_file_path = temp_file.name
                    temp_file.close()

                    logger.info("Creating safe database copy for upload: %s", temp_file_path)
                    shutil.copy2(str(database_dir), temp_file_path)

                    # Verify copy integrity
                    original_size = database_dir.stat().st_size
                    copy_size = Path(temp_file_path).stat().st_size

                    if original_size != copy_size:
                        logger.error("Database copy size mismatch: %s vs %s", original_size, copy_size)
                        if temp_file_path and os.path.exists(temp_file_path):
                            try:
                                os.unlink(temp_file_path)
                            except Exception as _e:
                                logger.debug("Suppressed temp-file cleanup exception: %s", _e)
                        return

                except Exception as copy_error:
                    logger.error("Error creating database copy: %s", copy_error, exc_info=True)
                    if temp_file_path and os.path.exists(temp_file_path):
                        try:
                            os.unlink(temp_file_path)
                        except Exception as _e:
                            logger.debug("Suppressed temp-file cleanup exception: %s", _e)
                    return

            # Upload the copy (outside the lock to avoid blocking database operations)
            if temp_file_path and os.path.exists(temp_file_path):
                try:
                    result = await self.screenshot_controller.screenshot_service.sync(
                        temp_file_path,
                        database_server,
                        user_entity
                    )

                    # Clean up temp file after upload
                    try:
                        os.unlink(temp_file_path)
                        logger.debug("Cleaned up temporary database copy: %s", temp_file_path)
                    except Exception as e:
                        logger.warning("Failed to clean up temp file: %s", e)

                    if self.screenshot_controller.is_link(result):
                        logger.info("Database uploaded successfully: %s", result)
                    else:
                        logger.error("Database upload failed: %s", result)
                except Exception as upload_error:
                    logger.error("Error uploading database copy: %s", upload_error, exc_info=True)
                    # Clean up temp file on upload error
                    try:
                        if temp_file and os.path.exists(temp_file.name):
                            os.unlink(temp_file.name)
                    except Exception as _e:
                        logger.debug("Suppressed temp-file cleanup exception: %s", _e)
            else:
                logger.error("Temporary database file not available for upload")

        except ValidationError as e:
            logger.error("Screenshot sync validation error for database: %s", e)
            logger.error("Context: file=%s, dest=%s", database_dir, database_server)
            # Continue processing - don't fail the entire operation
        except ScreenshotError as e:
            logger.error("Screenshot sync error for database: %s", e)
            logger.error("Context: file=%s, dest=%s", database_dir, database_server)
            # Continue processing - don't fail the entire operation
        except NetworkError as e:
            logger.error("Network error during screenshot sync for database: %s", e)
            logger.error("Context: file=%s, dest=%s", database_dir, database_server)
            # Continue processing - don't fail the entire operation
        except Exception as e:
            logger.error("Error in database upload: %s", e, exc_info=True)
            # Continue processing - don't fail the entire operation
