# encoding: utf-8

from __future__ import absolute_import, division, print_function, unicode_literals

import json
import os
import re
import shutil
import threading
import warnings

import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from datetime import datetime
from django.utils.encoding import force_str
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
from haystack.constants import DJANGO_CT, DJANGO_ID, ID
from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
from haystack.inputs import Clean, Exact, PythonData, Raw
from haystack.models import SearchResult
from haystack.utils import get_identifier, get_model_ct
from haystack.utils import log as logging
from haystack.utils.app_loading import haystack_get_model
from jieba.analyse import ChineseAnalyzer  # 使用结巴分词进行中文分词
from whoosh import index
from whoosh.analysis import StemmingAnalyzer
from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
from whoosh.fields import ID as WHOOSH_ID
from whoosh.filedb.filestore import FileStorage, RamStorage
from whoosh.highlight import ContextFragmenter, HtmlFormatter
from whoosh.highlight import highlight as whoosh_highlight
from whoosh.qparser import QueryParser
from whoosh.searching import ResultsPage
from whoosh.writing import AsyncWriter

try:
    import whoosh
except ImportError:
    raise MissingDependency(
        "The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")

# 检查 Whoosh 版本是否满足最低要求（2.5.0）
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
    raise MissingDependency(
        "The 'whoosh' backend requires version 2.5.0 or greater.")

# 使用线程局部存储，用于 RAM 存储模式下的共享存储
DATETIME_REGEX = re.compile(
    '^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(\.\d{3,6}Z?)?$')
LOCALS = threading.local()
LOCALS.RAM_STORE = None


class WhooshHtmlFormatter(HtmlFormatter):
    """
    自定义高亮格式化器，使用简单标签包裹匹配关键词。
    与 Solr、Elasticsearch 等后端保持一致的高亮样式。
    """
    template = '<%(tag)s>%(t)s</%(tag)s>'


class WhooshSearchBackend(BaseSearchBackend):
    """
    Whoosh 搜索后端实现类，负责与 Whoosh 引擎交互，执行索引、搜索、删除等操作。
    继承自 haystack 的 BaseSearchBackend。
    """

    # Whoosh 中的保留关键字，不能直接用于查询
    RESERVED_WORDS = (
        'AND',
        'NOT',
        'OR',
        'TO',
    )

    # Whoosh 中的保留字符，需要转义或加引号处理
    RESERVED_CHARACTERS = (
        '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}',
        '[', ']', '^', '"', '~', '*', '?', ':', '.',
    )

    def __init__(self, connection_alias, **connection_options):
        """
        初始化 Whoosh 后端连接。
        :param connection_alias: 连接别名（如 'default'）
        :param connection_options: 配置选项，如 PATH、STORAGE 等
        """
        super(WhooshSearchBackend, self).__init__(connection_alias, **connection_options)
        self.setup_complete = False  # 是否已完成初始化
        self.use_file_storage = True  # 是否使用文件存储（而非内存）
        self.post_limit = getattr(connection_options, 'POST_LIMIT', 128 * 1024 * 1024)  # POST 请求大小限制
        self.path = connection_options.get('PATH')  # 索引文件存储路径

        if connection_options.get('STORAGE', 'file') != 'file':
            self.use_file_storage = False  # 使用 RAM 存储

        if self.use_file_storage and not self.path:
            raise ImproperlyConfigured(
                "You must specify a 'PATH' in your settings for connection '%s'." % connection_alias)

        self.log = logging.getLogger('haystack')  # 日志记录器

    def setup(self):
        """
        初始化索引环境：创建目录、构建 schema、打开或创建索引。
        延迟加载，首次使用时调用。
        """
        from haystack import connections
        new_index = False

        # 创建索引目录
        if self.use_file_storage and not os.path.exists(self.path):
            os.makedirs(self.path)
            new_index = True

        # 检查写权限
        if self.use_file_storage and not os.access(self.path, os.W_OK):
            raise IOError(
                "The path to your Whoosh index '%s' is not writable for the current user/group." % self.path)

        # 选择存储方式：文件 or 内存
        if self.use_file_storage:
            self.storage = FileStorage(self.path)
        else:
            global LOCALS
            if getattr(LOCALS, 'RAM_STORE', None) is None:
                LOCALS.RAM_STORE = RamStorage()
            self.storage = LOCALS.RAM_STORE

        # 构建索引字段 schema
        self.content_field_name, self.schema = self.build_schema(
            connections[self.connection_alias].get_unified_index().all_searchfields())
        self.parser = QueryParser(self.content_field_name, schema=self.schema)  # 查询解析器

        # 打开或创建索引
        if new_index is True:
            self.index = self.storage.create_index(self.schema)
        else:
            try:
                self.index = self.storage.open_index(schema=self.schema)
            except index.EmptyIndexError:
                self.index = self.storage.create_index(self.schema)

        self.setup_complete = True

    def build_schema(self, fields):
        """
        根据 Django 模型字段定义构建 Whoosh 的 Schema（索引结构）。
        :param fields: 所有注册的 SearchField 字段
        :return: (content_field_name, schema) 内容字段名和 schema 对象
        """
        schema_fields = {
            ID: WHOOSH_ID(stored=True, unique=True),
            DJANGO_CT: WHOOSH_ID(stored=True),
            DJANGO_ID: WHOOSH_ID(stored=True),
        }
        initial_key_count = len(schema_fields)
        content_field_name = ''

        for field_name, field_class in fields.items():
            if field_class.is_multivalued:
                if field_class.indexed is False:
                    schema_fields[field_class.index_fieldname] = IDLIST(stored=True, field_boost=field_class.boost)
                else:
                    schema_fields[field_class.index_fieldname] = KEYWORD(
                        stored=True, commas=True, scorable=True, field_boost=field_class.boost)
            elif field_class.field_type in ['date', 'datetime']:
                schema_fields[field_class.index_fieldname] = DATETIME(stored=field_class.stored, sortable=True)
            elif field_class.field_type == 'integer':
                schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=int, field_boost=field_class.boost)
            elif field_class.field_type == 'float':
                schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=float, field_boost=field_class.boost)
            elif field_class.field_type == 'boolean':
                schema_fields[field_class.index_fieldname] = BOOLEAN(stored=field_class.stored)
            elif field_class.field_type == 'ngram':
                schema_fields[field_class.index_fieldname] = NGRAM(minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost)
            elif field_class.field_type == 'edge_ngram':
                schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost)
            else:
                # 默认文本字段，使用中文分词器
                schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
            if field_class.document is True:
                content_field_name = field_class.index_fieldname
                schema_fields[field_class.index_fieldname].spelling = True  # 支持拼写检查

        if len(schema_fields) <= initial_key_count:
            raise SearchBackendError("No fields were found in any search_indexes. Please correct this before attempting to search.")

        return (content_field_name, Schema(**schema_fields))

    def update(self, index, iterable, commit=True):
        """
        更新索引文档（添加或更新）。
        :param index: SearchIndex 实例
        :param iterable: 要索引的对象集合（如 QuerySet）
        :param commit: 是否立即提交（实际被忽略，Always commit）
        """
        if not self.setup_complete:
            self.setup()

        self.index = self.index.refresh()
        writer = AsyncWriter(self.index)  # 异步写入，避免锁

        for obj in iterable:
            try:
                doc = index.full_prepare(obj)  # 准备文档数据
            except SkipDocument:
                self.log.debug(u"Indexing for object `%s` skipped", obj)
            else:
                for key in doc:
                    doc[key] = self._from_python(doc[key])  # 转换为 Whoosh 可接受类型

                if 'boost' in doc:
                    del doc['boost']  # Whoosh 2.5+ 不支持文档级 boost

                try:
                    writer.update_document(**doc)
                except Exception as e:
                    if not self.silently_fail:
                        raise
                    self.log.error(u"%s while preparing object for update" % e.__class__.__name__, exc_info=True,
                                   extra={"data": {"index": index, "object": get_identifier(obj)}})

        if len(iterable) > 0:
            writer.commit()  # 提交写入

    def remove(self, obj_or_string, commit=True):
        """
        从索引中删除一个文档。
        :param obj_or_string: 要删除的对象或其 ID 字符串
        :param commit: 是否立即提交
        """
        if not self.setup_complete:
            self.setup()

        self.index = self.index.refresh()
        whoosh_id = get_identifier(obj_or_string)

        try:
            self.index.delete_by_query(q=self.parser.parse(u'%s:"%s"' % (ID, whoosh_id)))
        except Exception as e:
            if not self.silently_fail:
                raise
            self.log.error("Failed to remove document '%s' from Whoosh: %s", whoosh_id, e, exc_info=True)

    def clear(self, models=None, commit=True):
        """
        清空索引。可指定模型或清空全部。
        :param models: 要清除的模型列表
        :param commit: 是否提交
        """
        if not self.setup_complete:
            self.setup()

        self.index = self.index.refresh()

        if models is not None:
            assert isinstance(models, (list, tuple))

        try:
            if models is None:
                self.delete_index()  # 删除整个索引
            else:
                models_to_delete = []
                for model in models:
                    models_to_delete.append(u"%s:%s" % (DJANGO_CT, get_model_ct(model)))
                self.index.delete_by_query(q=self.parser.parse(u" OR ".join(models_to_delete)))
        except Exception as e:
            if not self.silently_fail:
                raise
            if models is not None:
                self.log.error("Failed to clear Whoosh index of models '%s': %s", ','.join(models_to_delete), e, exc_info=True)
            else:
                self.log.error("Failed to clear Whoosh index: %s", e, exc_info=True)

    def delete_index(self):
        """
        彻底删除索引目录并重建。
        """
        if self.use_file_storage and os.path.exists(self.path):
            shutil.rmtree(self.path)
        elif not self.use_file_storage:
            self.storage.clean()
        self.setup()  # 重新初始化

    def optimize(self):
        """
        优化索引（合并段），提升搜索性能。
        """
        if not self.setup_complete:
            self.setup()
        self.index = self.index.refresh()
        self.index.optimize()

    def calculate_page(self, start_offset=0, end_offset=None):
        """
        计算分页参数（页码和每页数量），适配 Whoosh 的分页机制。
        :return: (page_num, page_length)
        """
        if end_offset is not None and end_offset <= 0:
            end_offset = 1
        if end_offset is None:
            end_offset = 1000000
        if start_offset is None:
            start_offset = 0
        page_length = end_offset - start_offset
        page_num = int(start_offset / page_length) + 1  # Whoosh 页码从 1 开始
        return page_num, page_length

    @log_query
    def search(self, query_string, sort_by=None, start_offset=0, end_offset=None, fields='', highlight=False,
               facets=None, date_facets=None, query_facets=None, narrow_queries=None, spelling_query=None,
               within=None, dwithin=None, distance_point=None, models=None, limit_to_registered_models=None,
               result_class=None, **kwargs):
        """
        执行搜索查询。
        :param query_string: 查询字符串
        :param sort_by: 排序字段
        :param start_offset, end_offset: 分页偏移
        :param highlight: 是否高亮
        :param narrow_queries: 窄化查询（过滤）
        :param models: 限制模型范围
        :param result_class: 搜索结果类
        :return: { 'results': [...], 'hits': int, 'spelling_suggestion': str }
        """
        if not self.setup_complete:
            self.setup()

        if len(query_string) == 0:
            return {'results': [], 'hits': 0}

        query_string = force_str(query_string)

        if len(query_string) <= 1 and query_string != u'*':
            return {'results': [], 'hits': 0}

        reverse = False
        if sort_by is not None:
            # 处理排序方向（仅支持全升或全降）
            sort_by_list = []
            reverse_counter = 0
            for order_by in sort_by:
                if order_by.startswith('-'):
                    reverse_counter += 1
            if reverse_counter and reverse_counter != len(sort_by):
                raise SearchBackendError("Whoosh requires all order_by fields to use the same sort direction")
            for order_by in sort_by:
                if order_by.startswith('-'):
                    sort_by_list.append(order_by[1:])
                    if len(sort_by_list) == 1:
                        reverse = True
                else:
                    sort_by_list.append(order_by)
                    if len(sort_by_list) == 1:
                        reverse = False
            sort_by = sort_by_list[0]

        # 不支持 facets
        if facets is not None:
            warnings.warn("Whoosh does not handle faceting.", Warning, stacklevel=2)
        if date_facets is not None:
            warnings.warn("Whoosh does not handle date faceting.", Warning, stacklevel=2)
        if query_facets is not None:
            warnings.warn("Whoosh does not handle query faceting.", Warning, stacklevel=2)

        narrowed_results = None
        self.index = self.index.refresh()

        # 处理模型过滤
        if limit_to_registered_models is None:
            limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
        if models and len(models):
            model_choices = sorted(get_model_ct(model) for model in models)
        elif limit_to_registered_models:
            model_choices = self.build_models_list()
        else:
            model_choices = []

        if len(model_choices) > 0:
            if narrow_queries is None:
                narrow_queries = set()
            narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))

        narrow_searcher = None
        if narrow_queries is not None:
            narrow_searcher = self.index.searcher()
            for nq in narrow_queries:
                recent_narrowed_results = narrow_searcher.search(self.parser.parse(force_str(nq)), limit=None)
                if len(recent_narrowed_results) <= 0:
                    return {'results': [], 'hits': 0}
                if narrowed_results:
                    narrowed_results.filter(recent_narrowed_results)
                else:
                    narrowed_results = recent_narrowed_results

        self.index = self.index.refresh()

        if self.index.doc_count():
            searcher = self.index.searcher()
            parsed_query = self.parser.parse(query_string)
            if parsed_query is None:
                return {'results': [], 'hits': 0}

            page_num, page_length = self.calculate_page(start_offset, end_offset)

            search_kwargs = {
                'pagelen': page_length,
                'sortedby': sort_by,
                'reverse': reverse,
            }
            if narrowed_results is not None:
                search_kwargs['filter'] = narrowed_results

            try:
                raw_page = searcher.search_page(parsed_query, page_num, **search_kwargs)
            except ValueError:
                if not self.silently_fail:
                    raise
                return {'results': [], 'hits': 0, 'spelling_suggestion': None}

            if raw_page.pagenum < page_num:
                return {'results': [], 'hits': 0, 'spelling_suggestion': None}

            results = self._process_results(raw_page, highlight=highlight, query_string=query_string,
                                           spelling_query=spelling_query, result_class=result_class)
            searcher.close()
            if hasattr(narrow_searcher, 'close'):
                narrow_searcher.close()
            return results
        else:
            spelling_suggestion = self.create_spelling_suggestion(spelling_query or query_string) \
                if self.include_spelling else None
            return {'results': [], 'hits': 0, 'spelling_suggestion': spelling_suggestion}

    def more_like_this(self, model_instance, additional_query_string=None, start_offset=0, end_offset=None,
                       models=None, limit_to_registered_models=None, result_class=None, **kwargs):
        """
        基于某文档查找相似文档（"More Like This" 功能）。
        """
        if not self.setup_complete:
            self.setup()

        model_klass = model_instance._meta.concrete_model
        field_name = self.content_field_name
        narrow_queries = set()
        narrowed_results = None
        self.index = self.index.refresh()

        # 模型过滤逻辑同 search
        if limit_to_registered_models is None:
            limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True)
        if models and len(models):
            model_choices = sorted(get_model_ct(model) for model in models)
        elif limit_to_registered_models:
            model_choices = self.build_models_list()
        else:
            model_choices = []

        if len(model_choices) > 0:
            narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices]))
        if additional_query_string and additional_query_string != '*':
            narrow_queries.add(additional_query_string)

        narrow_searcher = None
        if narrow_queries:
            narrow_searcher = self.index.searcher()
            for nq in narrow_queries:
                recent_narrowed_results = narrow_searcher.search(self.parser.parse(force_str(nq)), limit=None)
                if len(recent_narrowed_results) <= 0:
                    return {'results': [], 'hits': 0}
                if narrowed_results:
                    narrowed_results.filter(recent_narrowed_results)
                else:
                    narrowed_results = recent_narrowed_results

        page_num, page_length = self.calculate_page(start_offset, end_offset)
        self.index = self.index.refresh()
        raw_results = EmptyResults()

        if self.index.doc_count():
            query = "%s:%s" % (ID, get_identifier(model_instance))
            searcher = self.index.searcher()
            parsed_query = self.parser.parse(query)
            results = searcher.search(parsed_query)
            if len(results):
                raw_results = results[0].more_like_this(field_name, top=end_offset)
            if narrowed_results and hasattr(raw_results, 'filter'):
                raw_results.filter(narrowed_results)
            try:
                raw_page = ResultsPage(raw_results, page_num, page_length)
            except ValueError:
                if not self.silently_fail:
                    raise
                return {'results': [], 'hits': 0}
            if raw_page.pagenum < page_num:
                return {'results': [], 'hits': 0}
            results = self._process_results(raw_page, result_class=result_class)
            searcher.close()
            if hasattr(narrow_searcher, 'close'):
                narrow_searcher.close()
            return results
        return {'results': [], 'hits': 0}

    def _process_results(self, raw_page, highlight=False, query_string='', spelling_query=None, result_class=None):
        """
        处理原始搜索结果，转换为 SearchResult 对象列表。
        :param raw_page: Whoosh 返回的原始结果页
        :param highlight: 是否生成高亮文本
        :return: { 'results': [...], 'hits': int, 'spelling_suggestion': str }
        """
        from haystack import connections
        results = []
        hits = len(raw_page)
        if result_class is None:
            result_class = SearchResult
        facets = {}
        spelling_suggestion = None
        unified_index = connections[self.connection_alias].get_unified_index()
        indexed_models = unified_index.get_indexed_models()

        for doc_offset, raw_result in enumerate(raw_page):
            score = raw_page.score(doc_offset) or 0
            app_label, model_name = raw_result[DJANGO_CT].split('.')
            additional_fields = {}
            model = haystack_get_model(app_label, model_name)

            if model and model in indexed_models:
                for key, value in raw_result.items():
                    index = unified_index.get_index(model)
                    string_key = str(key)
                    if string_key in index.fields and hasattr(index.fields[string_key], 'convert'):
                        if index.fields[string_key].is_multivalued:
                            additional_fields[string_key] = value.split(',') if value else []
                        else:
                            additional_fields[string_key] = index.fields[string_key].convert(value)
                    else:
                        additional_fields[string_key] = self._to_python(value)
                del additional_fields[DJANGO_CT]
                del additional_fields[DJANGO_ID]

                if highlight:
                    sa = StemmingAnalyzer()
                    formatter = WhooshHtmlFormatter('em')
                    terms = [token.text for token in sa(query_string)]
                    whoosh_result = whoosh_highlight(additional_fields.get(self.content_field_name),
                                                    terms, sa, ContextFragmenter(), formatter)
                    additional_fields['highlighted'] = {self.content_field_name: [whoosh_result]}

                result = result_class(app_label, model_name, raw_result[DJANGO_ID], score, **additional_fields)
                results.append(result)
            else:
                hits -= 1

        if self.include_spelling:
            spelling_suggestion = self.create_spelling_suggestion(spelling_query or query_string)

        return {
            'results': results,
            'hits': hits,
            'facets': facets,
            'spelling_suggestion': spelling_suggestion,
        }

    def create_spelling_suggestion(self, query_string):
        """
        生成拼写纠错建议。
        :param query_string: 原始查询字符串
        :return: 建议字符串或 None
        """
        if not query_string:
            return None
        reader = self.index.reader()
        corrector = reader.corrector(self.content_field_name)
        cleaned_query = force_str(query_string)
        for rev_word in self.RESERVED_WORDS:
            cleaned_query = cleaned_query.replace(rev_word, '')
        for rev_char in self.RESERVED_CHARACTERS:
            cleaned_query = cleaned_query.replace(rev_char, '')
        query_words = cleaned_query.split()
        suggested_words = []
        for word in query_words:
            suggestions = corrector.suggest(word, limit=1)
            if suggestions:
                suggested_words.append(suggestions[0])
        return ' '.join(suggested_words)

    def _from_python(self, value):
        """
        将 Python 值转换为 Whoosh 可索引的字符串。
        """
        if hasattr(value, 'strftime'):
            if not hasattr(value, 'hour'):
                value = datetime(value.year, value.month, value.day, 0, 0, 0)
        elif isinstance(value, bool):
            value = 'true' if value else 'false'
        elif isinstance(value, (list, tuple)):
            value = u','.join([force_str(v) for v in value])
        elif isinstance(value, (six.integer_types, float)):
            pass
        else:
            value = force_str(value)
        return value

    def _to_python(self, value):
        """
        将 Whoosh 存储的值转换回 Python 类型。
        """
        if value == 'true':
            return True
        elif value == 'false':
            return False
        if value and isinstance(value, six.string_types):
            possible_datetime = DATETIME_REGEX.search(value)
            if possible_datetime:
                date_values = possible_datetime.groupdict()
                for dk, dv in date_values.items():
                    date_values[dk] = int(dv)
                return datetime(**date_values)
        try:
            converted_value = json.loads(value)
            if isinstance(converted_value, (list, tuple, set, dict, six.integer_types, float, complex)):
                return converted_value
        except BaseException:
            pass
        return value


class WhooshSearchQuery(BaseSearchQuery):
    """
    Whoosh 查询构建器，负责将 Django 查询语法转换为 Whoosh 查询字符串。
    """

    def _convert_datetime(self, date):
        """将 datetime 转换为 Whoosh 可识别的字符串格式"""
        if hasattr(date, 'hour'):
            return force_str(date.strftime('%Y%m%d%H%M%S'))
        else:
            return force_str(date.strftime('%Y%m%d000000'))

    def clean(self, query_fragment):
        """
        清理用户输入的查询片段，避免保留字和字符引发语法错误。
        """
        words = query_fragment.split()
        cleaned_words = []
        for word in words:
            if word in self.backend.RESERVED_WORDS:
                word = word.lower()
            for char in self.backend.RESERVED_CHARACTERS:
                if char in word:
                    word = "'%s'" % word
                    break
            cleaned_words.append(word)
        return ' '.join(cleaned_words)

    def build_query_fragment(self, field, filter_type, value):
        """
        构建单个查询片段，如 "title:django" 或 "pub_date:[20200101 TO 20201231]"
        """
        from haystack import connections
        query_frag = ''
        is_datetime = False

        if not hasattr(value, 'input_type_name'):
            if hasattr(value, 'values_list'):
                value = list(value)
            if hasattr(value, 'strftime'):
                is_datetime = True
            if isinstance(value, six.string_types) and value != ' ':
                value = Clean(value)
            else:
                value = PythonData(value)

        prepared_value = value.prepare(self)
        if not isinstance(prepared_value, (set, list, tuple)):
            prepared_value = self.backend._from_python(prepared_value)

        index_fieldname = '' if field == 'content' else u'%s:' % connections[self._using].get_unified_index().get_index_fieldname(field)

        filter_types = {
            'content': '%s',
            'contains': '*%s*',
            'endswith': "*%s",
            'startswith': "%s*",
            'exact': '%s',
            'gt': "{%s to}",
            'gte': "[%s to]",
            'lt': "{to %s}",
            'lte': "[to %s]",
            'fuzzy': u'%s~',
        }

        if value.post_process is False:
            query_frag = prepared_value
        else:
            if filter_type in ['content', 'contains', 'startswith', 'endswith', 'fuzzy']:
                terms = []
                possible_values = prepared_value.split(' ') if isinstance(prepared_value, six.string_types) else [prepared_value]
                if is_datetime:
                    possible_values = [self._convert_datetime(pv) for pv in possible_values]
                for pv in possible_values:
                    terms.append(filter_types[filter_type] % self.backend._from_python(pv))
                query_frag = terms[0] if len(terms) == 1 else u"(%s)" % " AND ".join(terms)
            elif filter_type == 'in':
                in_options = []
                for pv in prepared_value:
                    is_dt = hasattr(pv, 'strftime')
                    pv = self.backend._from_python(pv)
                    pv = self._convert_datetime(pv) if is_dt else pv
                    in_options.append('"%s"' % pv if isinstance(pv, six.string_types) and not is_dt else '%s' % pv)
                query_frag = "(%s)" % " OR ".join(in_options)
            elif filter_type == 'range':
                start = self.backend._from_python(prepared_value[0])
                end = self.backend._from_python(prepared_value[1])
                start = self._convert_datetime(start) if hasattr(prepared_value[0], 'strftime') else start
                end = self._convert_datetime(end) if hasattr(prepared_value[1], 'strftime') else end
                query_frag = u"[%s to %s]" % (start, end)
            elif filter_type == 'exact':
                prepared_value = Exact(prepared_value).prepare(self)
                query_frag = filter_types[filter_type] % prepared_value
            else:
                if is_datetime:
                    prepared_value = self._convert_datetime(prepared_value)
                query_frag = filter_types[filter_type] % prepared_value

        if query_frag and not isinstance(value, Raw) and not query_frag.startswith('(') and not query_frag.endswith(')'):
            query_frag = "(%s)" % query_frag

        return u"%s%s" % (index_fieldname, query_frag)


class WhooshEngine(BaseEngine):
    """
    Haystack 引擎注册类，绑定 Backend 和 Query 类。
    """
    backend = WhooshSearchBackend
    query = WhooshSearchQuery