"""
History/diff rendering for text_json_diff processor.

This module handles the visualization of text/HTML/JSON changes by rendering
a side-by-side or unified diff view with syntax highlighting and change markers.
"""

import os
import time
from flask_babel import gettext
from loguru import logger

from changedetectionio import diff, strtobool
from changedetectionio.diff import (
    REMOVED_STYLE, ADDED_STYLE, REMOVED_INNER_STYLE, ADDED_INNER_STYLE,
    REMOVED_PLACEMARKER_OPEN, REMOVED_PLACEMARKER_CLOSED,
    ADDED_PLACEMARKER_OPEN, ADDED_PLACEMARKER_CLOSED,
    CHANGED_PLACEMARKER_OPEN, CHANGED_PLACEMARKER_CLOSED,
    CHANGED_INTO_PLACEMARKER_OPEN, CHANGED_INTO_PLACEMARKER_CLOSED
)
from changedetectionio.notification.handler import apply_html_color_to_body


def build_diff_cell_visualizer(content, resolution=100):
    """
    Build a visual cell grid for the diff visualizer.

    Analyzes the content for placemarkers indicating changes and creates a
    grid of cells representing the document, with each cell marked as:
    - 'deletion' for removed content
    - 'insertion' for added content
    - 'mixed' for cells containing both deletions and insertions
    - empty string for cells with no changes

    Args:
        content: The diff content with placemarkers
        resolution: Number of cells to create (default 100)

    Returns:
        List of dicts with 'class' key for each cell's CSS class
    """
    if not content:
        return [{'class': ''} for _ in range(resolution)]
    now = time.time()
    # Work with character positions for better accuracy
    content_length = len(content)

    if content_length == 0:
        return [{'class': ''} for _ in range(resolution)]

    chars_per_cell = max(1, content_length / resolution)

    # Track change type for each cell
    cell_data = {}

    # Placemarkers to detect
    change_markers = {
        REMOVED_PLACEMARKER_OPEN: 'deletion',
        ADDED_PLACEMARKER_OPEN: 'insertion',
        CHANGED_PLACEMARKER_OPEN: 'deletion',
        CHANGED_INTO_PLACEMARKER_OPEN: 'insertion',
    }

    # Find all occurrences of each marker
    for marker, change_type in change_markers.items():
        pos = 0
        while True:
            pos = content.find(marker, pos)
            if pos == -1:
                break

            # Calculate which cell this marker falls into
            cell_index = min(int(pos / chars_per_cell), resolution - 1)

            if cell_index not in cell_data:
                cell_data[cell_index] = change_type
            elif cell_data[cell_index] != change_type:
                # Mixed changes in this cell
                cell_data[cell_index] = 'mixed'

            pos += len(marker)

    # Build the cell list
    cells = []
    for i in range(resolution):
        change_type = cell_data.get(i, '')
        cells.append({'class': change_type})

    logger.debug(f"Built diff cell visualizer: {len([c for c in cells if c['class']])} cells with changes out of {resolution} in {time.time() - now:.2f}s")

    return cells

# Diff display preferences configuration - single source of truth
DIFF_PREFERENCES_CONFIG = {
    'changesOnly': {'default': True, 'type': 'bool'},
    'ignoreWhitespace': {'default': False, 'type': 'bool'},
    'removed': {'default': True, 'type': 'bool'},
    'added': {'default': True, 'type': 'bool'},
    'replaced': {'default': True, 'type': 'bool'},
    'type': {'default': 'diffLines', 'type': 'value'},
    'llm_all_changes': {'default': False, 'type': 'bool'},
}

def render(watch, datastore, request, url_for, render_template, flash, redirect, extract_form=None):
    """
    Render the history/diff view for text/JSON/HTML changes.

    Args:
        watch: The watch object
        datastore: The ChangeDetectionStore instance
        request: Flask request object
        url_for: Flask url_for function
        render_template: Flask render_template function
        flash: Flask flash function
        redirect: Flask redirect function
        extract_form: Optional pre-built extract form (for error cases)

    Returns:
        Rendered HTML response
    """
    from changedetectionio import forms

    uuid = watch.get('uuid')

    extra_stylesheets = [url_for('static_content', group='styles', filename='diff.css')]

    # Use provided form or create a new one
    if extract_form is None:
        extract_form = forms.extractDataForm(formdata=request.form,
                                             data={'extract_regex': request.form.get('extract_regex', '')}
                                             )
    history = watch.history
    dates = list(history.keys())

    # If a "from_version" was requested, then find it (or the closest one)
    # Also set "from version" to be the closest version to the one that was last viewed.

    best_last_viewed_timestamp = watch.get_from_version_based_on_last_viewed
    from_version_timestamp = best_last_viewed_timestamp if best_last_viewed_timestamp else dates[-2]
    from_version = request.args.get('from_version', from_version_timestamp )

    # Use the current one if nothing was specified
    to_version = request.args.get('to_version', str(dates[-1]))

    try:
        to_version_file_contents = watch.get_history_snapshot(timestamp=to_version)
    except Exception as e:
        logger.error(f"Unable to read watch history to-version for version {to_version}: {str(e)}")
        to_version_file_contents = f"Unable to read to-version at {to_version}.\n"

    try:
        from_version_file_contents = watch.get_history_snapshot(timestamp=from_version)
    except Exception as e:
        logger.error(f"Unable to read watch history from-version for version {from_version}: {str(e)}")
        from_version_file_contents = f"Unable to read to-version {from_version}.\n"

    screenshot_url = watch.get_screenshot()

    is_html_webdriver = watch.fetcher_supports_screenshots

    password_enabled_and_share_is_off = False
    if datastore.data['settings']['application'].get('password') or os.getenv("SALTED_PASS", False):
        password_enabled_and_share_is_off = not datastore.data['settings']['application'].get('shared_diff_access')

    datastore.set_last_viewed(uuid, time.time())

    # Parse diff preferences from request using config as single source of truth
    # Check if this is a user submission (any diff pref param exists in query string)
    user_submitted = any(key in request.args for key in DIFF_PREFERENCES_CONFIG.keys())

    diff_prefs = {}
    for key, config in DIFF_PREFERENCES_CONFIG.items():
        if user_submitted:
            # User submitted form - missing checkboxes are explicitly OFF
            if config['type'] == 'bool':
                diff_prefs[key] = strtobool(request.args.get(key, 'off'))
            else:
                diff_prefs[key] = request.args.get(key, config['default'])
        else:
            # Initial load - use defaults from config
            diff_prefs[key] = config['default']

    content = diff.render_diff(previous_version_file_contents=from_version_file_contents,
                               newest_version_file_contents=to_version_file_contents,
                               include_replaced=diff_prefs['replaced'],
                               include_added=diff_prefs['added'],
                               include_removed=diff_prefs['removed'],
                               include_equal=diff_prefs['changesOnly'],
                               ignore_junk=diff_prefs['ignoreWhitespace'],
                               word_diff=diff_prefs['type'] == 'diffWords',
                               )

    # Build cell grid visualizer before applying HTML color (so we can detect placemarkers)
    diff_cell_grid = build_diff_cell_visualizer(content)

    content = apply_html_color_to_body(n_body=content)
    offscreen_content = render_template("diff-offscreen-options.html")

    note = ''
    if str(from_version) != str(dates[-2]) or str(to_version) != str(dates[-1]):
        note = 'Note: You are not viewing the latest changes.'

    llm_configured = bool(
        datastore.data.get('settings', {}).get('application', {}).get('llm', {}).get('model')
    )

    # Load cached AI diff summary for this exact from→to + prompt combination
    viewing_latest = str(to_version) == str(dates[-1])
    llm_diff_summary = ''
    llm_summary_prompt = ''
    if llm_configured:
        try:
            from changedetectionio.llm.evaluator import (
                get_effective_summary_prompt, build_summary_cache_prompt,
            )
            _prompt = get_effective_summary_prompt(watch, datastore)
            llm_summary_prompt = _prompt
            # Must match the cache_prompt the worker writes and the UI ajax route reads —
            # using UI default diff prefs so the initial render finds the worker's pre-cache.
            from changedetectionio.llm.evaluator import get_llm_settings
            _ls = get_llm_settings(datastore)
            _max_summary_tokens = _ls.max_summary_tokens
            _llm_model = _ls.model
            _cache_prompt = build_summary_cache_prompt(
                effective_prompt=_prompt,
                max_summary_tokens=_max_summary_tokens,
                model=_llm_model,
            )
            llm_diff_summary = watch.get_llm_diff_summary(from_version, to_version, prompt=_cache_prompt)
        except Exception as e:
            logger.warning(f"Could not load llm-diff-summary for {uuid}: {e}")

    output = render_template("diff.html",
                             #initial_scroll_line_number=100,
                             bottom_horizontal_offscreen_contents=offscreen_content,
                             content=content,
                             current_diff_url=watch['url'],
                             diff_cell_grid=diff_cell_grid,
                             diff_prefs=diff_prefs,
                             extra_classes=' '.join(filter(None, ['difference-page', 'llm-configured' if llm_configured else ''])),
                             extra_stylesheets=extra_stylesheets,
                             extra_title=f" - {watch.label} - {gettext('History')}",
                             extract_form=extract_form,
                             from_version=str(from_version),
                             is_html_webdriver=is_html_webdriver,
                             last_error=watch['last_error'],
                             last_error_screenshot=watch.get_error_snapshot(),
                             last_error_text=watch.get_error_text(),
                             newest=to_version_file_contents,
                             newest_version_timestamp=dates[-1],
                             note=note,
                             password_enabled_and_share_is_off=password_enabled_and_share_is_off,
                             pure_menu_fixed=False,
                             screenshot=screenshot_url,
                             to_version=str(to_version),
                             uuid=uuid,
                             versions=dates,  # All except current/last
                             watch_a=watch,
                             llm_configured=llm_configured,
                             llm_diff_summary=llm_diff_summary,
                             llm_summary_prompt=llm_summary_prompt,
                             viewing_latest=viewing_latest,
                             )
    return output
