import time
import threading
from blinker import signal
from flask import Blueprint, request, redirect, url_for, flash, render_template, session, current_app
from flask_babel import gettext
from loguru import logger

from changedetectionio.store import ChangeDetectionStore
from changedetectionio.blueprint.ui.edit import construct_blueprint as construct_edit_blueprint
from changedetectionio.blueprint.ui.notification import construct_blueprint as construct_notification_blueprint
from changedetectionio.blueprint.ui.views import construct_blueprint as construct_views_blueprint
from changedetectionio.blueprint.ui.queue import construct_blueprint as construct_queue_blueprint
from changedetectionio.blueprint.ui import diff, preview

def _handle_operations(op, uuids, datastore, worker_pool, update_q, queuedWatchMetaData, watch_check_update, extra_data=None, emit_flash=True):
    """Apply a bulk operation to the given watch uuids.

    Returns a {'message': str, 'type': 'success'|'error'} result describing the outcome so callers
    (e.g. the socket.io handler) can surface server-driven feedback. When emit_flash is True the same
    message is also flashed for the classic form-POST + redirect path.
    """
    from flask import flash

    # Result feedback - 'success' unless a branch overrides it (e.g. invalid input)
    result_message = None
    result_type = 'success'

    if op == 'delete':
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.delete(uuid)
        result_message = gettext("{} watches deleted").format(len(uuids))

    elif op == 'pause':
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.data['watching'][uuid]['paused'] = True
                datastore.data['watching'][uuid].commit()
        result_message = gettext("{} watches paused").format(len(uuids))

    elif op == 'unpause':
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.data['watching'][uuid.strip()]['paused'] = False
                datastore.data['watching'][uuid].commit()
        result_message = gettext("{} watches unpaused").format(len(uuids))

    elif (op == 'mark-viewed'):
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.set_last_viewed(uuid, int(time.time()))
        result_message = gettext("{} watches updated").format(len(uuids))

    elif (op == 'mute'):
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.data['watching'][uuid]['notification_muted'] = True
                datastore.data['watching'][uuid].commit()
        result_message = gettext("{} watches muted").format(len(uuids))

    elif (op == 'unmute'):
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.data['watching'][uuid]['notification_muted'] = False
                datastore.data['watching'][uuid].commit()
        result_message = gettext("{} watches un-muted").format(len(uuids))

    elif (op == 'recheck'):
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                # Recheck and require a full reprocessing
                worker_pool.queue_item_async_safe(update_q, queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
        result_message = gettext("{} watches queued for rechecking").format(len(uuids))

    elif (op == 'clear-errors'):
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.data['watching'][uuid]["last_error"] = False
                datastore.data['watching'][uuid].commit()
        result_message = gettext("{} watches errors cleared").format(len(uuids))

    elif (op == 'clear-history'):
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.clear_watch_history(uuid)
        result_message = gettext("{} watches cleared/reset.").format(len(uuids))

    elif (op == 'notification-default'):
        from changedetectionio.notification import (
            USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH
        )
        for uuid in uuids:
            if datastore.data['watching'].get(uuid):
                datastore.data['watching'][uuid]['notification_title'] = None
                datastore.data['watching'][uuid]['notification_body'] = None
                datastore.data['watching'][uuid]['notification_urls'] = []
                datastore.data['watching'][uuid]['notification_format'] = USE_SYSTEM_DEFAULT_NOTIFICATION_FORMAT_FOR_WATCH
                datastore.data['watching'][uuid].commit()
        result_message = gettext("{} watches set to use default notification settings").format(len(uuids))

    elif (op == 'set-fetch-backend'):
        # extra_data = the chosen fetch backend key (e.g. 'html_requests', 'html_webdriver')
        from changedetectionio import content_fetchers
        # 'system' = inherit the global default fetch method (resolved at fetch time), same as the edit page
        valid = [f[0] for f in content_fetchers.available_fetchers()] + ['system']
        if extra_data in valid:
            for uuid in uuids:
                if datastore.data['watching'].get(uuid):
                    datastore.data['watching'][uuid]['fetch_backend'] = extra_data
                    datastore.data['watching'][uuid].commit()
            result_message = gettext("Browser / fetch method updated on {} watches").format(len(uuids))
        else:
            result_message = gettext("Invalid browser / fetch method selected")
            result_type = 'error'

    elif (op == 'set-proxy'):
        # extra_data = proxy key; '' means the system default proxy
        valid = [''] + (list(datastore.proxy_list.keys()) if datastore.proxy_list else [])
        if (extra_data or '') in valid:
            for uuid in uuids:
                if datastore.data['watching'].get(uuid):
                    datastore.data['watching'][uuid]['proxy'] = (extra_data or '')
                    datastore.data['watching'][uuid].commit()
            result_message = gettext("Proxy updated on {} watches").format(len(uuids))
        else:
            result_message = gettext("Invalid proxy selected")
            result_type = 'error'

    elif (op == 'assign-tag'):
        op_extradata = extra_data
        if op_extradata:
            tag_uuid = datastore.add_tag(title=op_extradata)
            if op_extradata and tag_uuid:
                for uuid in uuids:
                    if datastore.data['watching'].get(uuid):
                        # Bug in old versions caused by bad edit page/tag handler
                        if isinstance(datastore.data['watching'][uuid]['tags'], str):
                            datastore.data['watching'][uuid]['tags'] = []

                        datastore.data['watching'][uuid]['tags'].append(tag_uuid)
                        datastore.data['watching'][uuid].commit()
        result_message = gettext("{} watches were tagged").format(len(uuids))

    if uuids:
        for uuid in uuids:
            watch_check_update.send(watch_uuid=uuid)

    # Classic form-POST path flashes (shown after the redirect); the socket path uses the returned value
    if result_message and emit_flash:
        if result_type == 'error':
            flash(result_message, 'error')
        else:
            flash(result_message)

    return {'message': result_message, 'type': result_type} if result_message else None

def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_pool, queuedWatchMetaData, watch_check_update):
    ui_blueprint = Blueprint('ui', __name__, template_folder="templates")
    
    # Register the edit blueprint
    edit_blueprint = construct_edit_blueprint(datastore, update_q, queuedWatchMetaData)
    ui_blueprint.register_blueprint(edit_blueprint)
    
    # Register the notification blueprint
    notification_blueprint = construct_notification_blueprint(datastore)
    ui_blueprint.register_blueprint(notification_blueprint)
    
    # Register the views blueprint
    views_blueprint = construct_views_blueprint(datastore, update_q, queuedWatchMetaData, watch_check_update)
    ui_blueprint.register_blueprint(views_blueprint)

    # Register diff and preview blueprints
    diff_blueprint = diff.construct_blueprint(datastore)
    ui_blueprint.register_blueprint(diff_blueprint)

    preview_blueprint = preview.construct_blueprint(datastore)
    ui_blueprint.register_blueprint(preview_blueprint)

    queue_blueprint = construct_queue_blueprint(datastore, update_q)
    ui_blueprint.register_blueprint(queue_blueprint)

    # Import the login decorator
    from changedetectionio.auth_decorator import login_optionally_required

    @ui_blueprint.route("/clear_history/<uuid_str:uuid>", methods=['POST'])
    @login_optionally_required
    def clear_watch_history(uuid):
        try:
            datastore.clear_watch_history(uuid)
        except KeyError:
            flash(gettext('Watch not found'), 'error')
        else:
            flash(gettext("Cleared snapshot history for watch {}").format(uuid))
        return redirect(url_for('watchlist.index'))

    @ui_blueprint.route("/clear_history", methods=['GET', 'POST'])
    @login_optionally_required
    def clear_all_history():
        if request.method == 'POST':
            confirmtext = request.form.get('confirmtext', '')

            if confirmtext.strip().lower() == gettext('clear').strip().lower():
                # Run in background thread to avoid blocking
                def clear_history_background():
                    # Capture UUIDs first to avoid race conditions
                    watch_uuids = list(datastore.data['watching'].keys())
                    logger.info(f"Background: Clearing history for {len(watch_uuids)} watches")

                    for uuid in watch_uuids:
                        try:
                            datastore.clear_watch_history(uuid)
                        except Exception as e:
                            logger.error(f"Error clearing history for watch {uuid}: {e}")

                    logger.info("Background: Completed clearing history")

                # Start daemon thread
                threading.Thread(target=clear_history_background, daemon=True).start()

                flash(gettext("History clearing started in background"))
            else:
                flash(gettext('Incorrect confirmation text.'), 'error')

            return redirect(url_for('watchlist.index'))

        output = render_template("clear_all_history.html")
        return output

    # Clear all statuses, so we do not see the 'unviewed' class
    @ui_blueprint.route("/form/mark-all-viewed", methods=['POST'])
    @login_optionally_required
    def mark_all_viewed():
        # Save the current newest history as the most recently viewed. Operate on
        # the SAME set the watch list is currently showing (tag/processor/status/
        # search) — via the shared filter — so a filtered "Mark all viewed" only
        # touches what's in view, not every watch.
        from changedetectionio.blueprint.watchlist import filters as wl_filters
        list_filters = wl_filters.list_filters_from_args(datastore, request.args)
        now = int(time.time())

        # Runs SYNCHRONOUSLY, and must stay that way. Re #4021: this used to hand the work to a
        # background thread and redirect immediately, so the watch list re-rendered from a
        # datastore that was still being marked and showed rows as unviewed until a manual
        # refresh. The realtime events that would have corrected it were emitted while the
        # browser was mid-navigation with no socket connected, so they went nowhere.
        # It is cheap enough to do inline: the per-watch signal is suppressed below (that was
        # the actual cost, not the disk write, which measures ~0.05ms per watch).
        marked_count = 0
        try:
            for watch_uuid, watch in datastore.data['watching'].items():
                if not wl_filters.watch_matches_filters(datastore, watch, list_filters):
                    continue

                datastore.set_last_viewed(watch_uuid, now, send_signal=False)
                marked_count += 1

            logger.info(f"Marking complete: {marked_count} watches marked as viewed")
        except Exception as e:
            logger.error(f"Error marking as viewed: {e}")

        # One summary event instead of one per watch, so other open tabs refresh their counters.
        # This page doesn't need it - the redirect below re-renders it from the marked datastore.
        if marked_count:
            general_stats_update = signal('general_stats_update')
            if general_stats_update:
                general_stats_update.send()

        return redirect(url_for('watchlist.index', **wl_filters.filter_query_args(request.args)))

    @ui_blueprint.route("/delete", methods=['POST'])
    @login_optionally_required
    def form_delete():
        uuid = request.args.get('uuid')
        # More for testing, possible to return the first/only
        if uuid == 'first':
            uuid = list(datastore.data['watching'].keys()).pop()

        if uuid != 'all' and not uuid in datastore.data['watching'].keys():
            flash(gettext('The watch by UUID {} does not exist.').format(uuid), 'error')
            return redirect(url_for('watchlist.index'))

        datastore.delete(uuid)
        flash(gettext('Deleted.'))

        return redirect(url_for('watchlist.index'))

    @ui_blueprint.route("/clone", methods=['POST'])
    @login_optionally_required
    def form_clone():
        uuid = request.args.get('uuid')

        if uuid == 'first':
            uuid = list(datastore.data['watching'].keys()).pop()

        new_uuid = datastore.clone(uuid)

        if not datastore.data['watching'].get(uuid).get('paused'):
            worker_pool.queue_item_async_safe(update_q, queuedWatchMetaData.PrioritizedItem(priority=5, item={'uuid': new_uuid}))

        flash(gettext('Cloned, you are editing the new watch.'))

        return redirect(url_for("ui.ui_edit.edit_page", uuid=new_uuid))

    @ui_blueprint.route("/checknow", methods=['POST'])
    @login_optionally_required
    def form_watch_checknow():
        # Forced recheck will skip the 'skip if content is the same' rule (, 'reprocess_existing_data': True})))
        from changedetectionio.blueprint.watchlist import filters as wl_filters
        uuid = request.args.get('uuid')

        if uuid:
            # Single watch - check if already queued or running
            if worker_pool.is_watch_running(uuid) or uuid in update_q.get_queued_uuids():
                flash(gettext("Watch is already queued or being checked."))
            else:
                worker_pool.queue_item_async_safe(update_q, queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
                flash(gettext("Queued 1 watch for rechecking."))
        else:
            # Multiple watches - operate on the SAME set the watch list is showing
            # (tag/processor/status/search) via the shared filter, skipping paused.
            list_filters = wl_filters.list_filters_from_args(datastore, request.args)
            watches_to_queue = []
            for k in sorted(datastore.data['watching'].items(), key=lambda item: item[1].get('last_checked', 0)):
                watch_uuid = k[0]
                watch = k[1]
                if not watch['paused'] and watch_uuid and wl_filters.watch_matches_filters(datastore, watch, list_filters):
                    watches_to_queue.append(watch_uuid)

            # If less than 20 watches, queue synchronously for immediate feedback
            if len(watches_to_queue) < 20:
                # Get already queued/running UUIDs once (efficient)
                queued_uuids = set(update_q.get_queued_uuids())
                running_uuids = set(worker_pool.get_running_uuids())

                # Filter out watches that are already queued or running
                watches_to_queue_filtered = []
                for watch_uuid in watches_to_queue:
                    if watch_uuid not in queued_uuids and watch_uuid not in running_uuids:
                        watches_to_queue_filtered.append(watch_uuid)

                # Queue only the filtered watches
                for watch_uuid in watches_to_queue_filtered:
                    worker_pool.queue_item_async_safe(update_q, queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid}))

                # Provide feedback about skipped watches
                skipped_count = len(watches_to_queue) - len(watches_to_queue_filtered)
                if skipped_count > 0:
                    flash(gettext("Queued {count} watches for rechecking ({skipped_count} already queued or running).").format(
                        count=len(watches_to_queue_filtered), skipped_count=skipped_count))
                else:
                    if len(watches_to_queue_filtered) == 1:
                        flash(gettext("Queued 1 watch for rechecking."))
                    else:
                        flash(gettext("Queued {} watches for rechecking.").format(len(watches_to_queue_filtered)))
            else:
                # 20+ watches - queue in background thread to avoid blocking HTTP response
                # Capture queued/running state before background thread
                queued_uuids = set(update_q.get_queued_uuids())
                running_uuids = set(worker_pool.get_running_uuids())

                def queue_watches_background():
                    """Background thread to queue watches - discarded after completion."""
                    try:
                        queued_count = 0
                        skipped_count = 0
                        for watch_uuid in watches_to_queue:
                            # Check if already queued or running (state captured at start)
                            if watch_uuid not in queued_uuids and watch_uuid not in running_uuids:
                                worker_pool.queue_item_async_safe(update_q, queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid}))
                                queued_count += 1
                            else:
                                skipped_count += 1

                        logger.info(f"Background queueing complete: {queued_count} watches queued, {skipped_count} skipped (already queued/running)")
                    except Exception as e:
                        logger.error(f"Error in background queueing: {e}")

                # Start background thread and return immediately
                thread = threading.Thread(target=queue_watches_background, daemon=True, name="QueueWatches-Background")
                thread.start()

                # Return immediately with approximate message
                flash(gettext("Queueing watches for rechecking in background..."))

        return redirect(url_for('watchlist.index', **wl_filters.filter_query_args(request.args)))

    @ui_blueprint.route("/form/checkbox-operations", methods=['POST'])
    @login_optionally_required
    def form_watch_list_checkbox_operations():
        op = request.form['op']
        uuids = [u.strip() for u in request.form.getlist('uuids') if u]
        extra_data = request.form.get('op_extradata', '').strip()
        _handle_operations(
            datastore=datastore,
            extra_data=extra_data,
            queuedWatchMetaData=queuedWatchMetaData,
            uuids=uuids,
            worker_pool=worker_pool,
            update_q=update_q,
            watch_check_update=watch_check_update,
            op=op,
        )

        return redirect(url_for('watchlist.index'))


    @ui_blueprint.route("/share-url/<uuid_str:uuid>", methods=['GET'])
    @login_optionally_required
    def form_share_put_watch(uuid):
        """Given a watch UUID, upload the info and return a share-link
           the share-link can be imported/added"""
        import requests
        import json
        from copy import deepcopy


        # copy it to memory as trim off what we dont need (history)
        watch = deepcopy(datastore.data['watching'].get(uuid))
        # For older versions that are not a @property
        if (watch.get('history')):
            del (watch['history'])

        # for safety/privacy
        for k in list(watch.keys()):
            if k.startswith('notification_'):
                del watch[k]

        for r in['uuid', 'last_checked', 'last_changed']:
            if watch.get(r):
                del (watch[r])

        # Add the global stuff which may have an impact
        watch['ignore_text'] += datastore.data['settings']['application']['global_ignore_text']
        watch['subtractive_selectors'] += datastore.data['settings']['application']['global_subtractive_selectors']

        watch_json = json.dumps(watch)

        try:
            r = requests.request(method="POST",
                                 data={'watch': watch_json},
                                 url="https://changedetection.io/share/share",
                                 headers={'App-Guid': datastore.data['app_guid']})
            res = r.json()

            # Add to the flask session
            session['share-link'] = f"https://changedetection.io/share/{res['share_key']}"


        except Exception as e:
            logger.error(f"Error sharing -{str(e)}")
            flash(gettext("Could not share, something went wrong while communicating with the share server - {}").format(str(e)), 'error')

        return redirect(url_for('watchlist.index'))

    @ui_blueprint.route("/language/auto-detect", methods=['GET'])
    def delete_locale_language_session_var_if_it_exists():
        """Clear the session locale preference to auto-detect from browser Accept-Language header"""
        if 'locale' in session:
            session.pop('locale', None)
            # Refresh Flask-Babel to clear cached locale
            from flask_babel import refresh
            refresh()
            flash(gettext("Language set to auto-detect from browser"))

        # Check if there's a redirect parameter to return to the same page
        redirect_url = request.args.get('redirect')

        # If redirect is provided and safe, use it
        from changedetectionio.is_safe_url import is_safe_url
        if redirect_url and is_safe_url(redirect_url, current_app):
            return redirect(redirect_url)

        # Otherwise redirect to watchlist
        return redirect(url_for('watchlist.index'))

    return ui_blueprint