import os
import re
import yaml
from typing import Any, Dict, List

import jinja2

from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.directives import ObjectDescription
from sphinx.util import logging, ws_re
from sphinx.util.display import status_iterator
from sphinx.util.docfields import Field
from sphinx.util.docutils import switch_source_input, SphinxDirective
from sphinx.util.nodes import make_id, nested_parse_with_titles
from sphinx.jinja2glue import BuiltinTemplateLoader
from docutils import nodes
from docutils.parsers.rst import directives
from docutils.statemachine import StringList

logger = logging.getLogger(__name__)

class DBConfigParser:

    """
    Regex pattern for parsing group comments.
    """
    GROUP_REGEX_PATTERN = (
        r'/\*\*\s*'
        r'\*+\s*@Group\s+(.*?)\s*'
        r'(?:\*+\s*@GroupDescription\s+(.*?)\s*)?'
        r'\*/'
    )

    """
    Regex pattern for parsing the configuration properties.
    """
    CONFIG_CC_REGEX_PATTERN = (
        r',\s*(\w+)\(this,'  # 0. Property name
        r'\s*"([^"]*)",\s*'  # 1. Property key
        r'(?:\s*"([^"]+)",)?'  # 2. Property alias (optional)
        r'\s*(?:liveness::(\w+),\s*)?'  # 3. Liveness (optional)
        r'(?:value_status::)?(\w+),\s*'  # 4. Value status
        r'(\{[^{}]*\}|"[^"]*"|[^,]+)?,'  # 5. Default value
        r'\s*"(.*?)"'  # 6. Description text, divided in multiple lines.
        r'(?:,\s*(.+?))?'  # 7. Available values (optional)
        r'\s*\)'
    )

    """
    Regex pattern for parsing named values in the configuration header file:
    """
    CONFIG_H_REGEX_PATTERN = r"\s*named_value<([\w:<>,\s]+)> (\w+);"

    """
    Regex pattern for parsing comments.
    """
    COMMENT_PATTERN = r"/\*.*?\*/|//.*?$"

    all_properties = {}

    def __init__(self, config_file_path, config_header_file_path):
        self.config_file_path = config_file_path
        self.config_header_file_path = config_header_file_path

    def _clean_comments(self, content):
        return re.sub(self.COMMENT_PATTERN, "", content, flags=re.DOTALL | re.MULTILINE)

    def _parse_group(self, group_match, config_group_content):
        group_name = group_match.group(1).strip()
        group_description = group_match.group(2).strip() if group_match.group(2) else ""

        current_group = {
            "name": group_name,
            "description": group_description,
            "properties": [],
            "value_status_count": {
                'Used': 0,
                'Unused': 0,
                'Invalid': 0,
                'Deprecated': 0,
            },
        }

        cleaned_properties_content = self._clean_comments(config_group_content)
        properties = self._parse_properties(cleaned_properties_content)
        current_group['properties'] = properties

        for prop in properties:
            value_status = prop['value_status']
            if value_status in current_group['value_status_count']:
                current_group['value_status_count'][value_status] += 1
        return current_group


    def _parse_properties(self, content):
        properties = []
        config_matches = re.findall(self.CONFIG_CC_REGEX_PATTERN, content, re.DOTALL)

        for match in config_matches:
            name = match[1].strip()
            property_data = {
                "name": name,
                "value_status": match[4].strip(),
                "default": match[5].strip(),
                "liveness": "True" if match[3] else "False",
                "description": match[6].strip(),
            }
            properties.append(property_data)
            DBConfigParser.all_properties[name] = property_data

        return properties

    def _add_type_information(self, groups):
        with open(self.config_header_file_path, "r") as file:
            config_header_content = file.read()

        config_header_matches = re.findall(self.CONFIG_H_REGEX_PATTERN, config_header_content)

        for match in config_header_matches:
            property_key = match[1].strip()
            for group in groups:
                for property_data in group["properties"]:
                    if property_data["name"] == property_key:
                        property_data["type"] = match[0].strip()

    def parse(self):
        groups = []

        with open(self.config_file_path, "r", encoding='utf-8') as file:
            config_content = file.read()

        group_matches = list(re.finditer(self.GROUP_REGEX_PATTERN, config_content, re.DOTALL))
        group_matches.append(None)  # Add a sentinel to process the last group

        for i in range(len(group_matches) - 1):
            group_match = group_matches[i]
            next_group_match = group_matches[i + 1]

            end_pos = next_group_match.start() if next_group_match else len(config_content)
            config_group_content = config_content[group_match.end():end_pos]
            
            current_group = self._parse_group(group_match, config_group_content)
            groups.append(current_group)

        self._add_type_information(groups)

        return groups

    @classmethod
    def get(cls, name: str):
        return DBConfigParser.all_properties[name]


def readable_desc(description: str) -> str:
    """
    This function is deprecated and maintained only for backward compatibility 
    with previous versions. Use ``readable_desc_rst``instead.
    """
    return (
        description.replace("\\n", "")
        .replace('<', '&lt;')
        .replace('>', '&gt;')
        .replace("\n", "<br>")
        .replace("\\t", "- ")
        .replace('"', "")
    )


def readable_desc_rst(description):
    indent = ' ' * 3
    lines = description.split('\n')
    cleaned_lines = []
    
    for line in lines:

        cleaned_line = line.replace('\\n', '\n')

        if line.endswith('"'):
            cleaned_line = cleaned_line[:-1] + ' '

        cleaned_line = cleaned_line.lstrip()
        cleaned_line = cleaned_line.replace('"', '')
        
        if cleaned_line != '':
            cleaned_line = indent + cleaned_line
            cleaned_lines.append(cleaned_line)
    
    return ''.join(cleaned_lines)


def maybe_add_filters(builder):
    env = builder.templates.environment
    if 'readable_desc' not in env.filters:
        env.filters['readable_desc'] = readable_desc

    if 'readable_desc_rst' not in env.filters:
        env.filters['readable_desc_rst'] = readable_desc_rst


class ConfigOption(ObjectDescription):
    has_content = True
    required_arguments = 1
    optional_arguments = 0
    final_argument_whitespace = False

    # TODO: instead of overriding transform_content(), render option properties
    #       as a field list.
    doc_field_types = [
        Field('type',
              label='Type',
              has_arg=False,
              names=('type',)),
        Field('default',
              label='Default value',
              has_arg=False,
              names=('default',)),
        Field('liveness',
              label='Liveness',
              has_arg=False,
              names=('liveness',)),
    ]

    def handle_signature(self,
                         sig: str,
                         signode: addnodes.desc_signature) -> str:
        signode.clear()
        signode += addnodes.desc_name(sig, sig)
        # normalize whitespace like XRefRole does
        return ws_re.sub(' ', sig)

    @property
    def env(self):
        document = self.state.document
        return document.settings.env

    def before_content(self) -> None:
        maybe_add_filters(self.env.app.builder)

    def _render(self, name) -> str:
        item = DBConfigParser.get(name)
        if item is None:
            raise self.error(f'Option "{name}" not found!')
        builder = self.env.app.builder
        template = self.config.scylladb_cc_properties_option_tmpl
        return builder.templates.render(template, item)

    def transform_content(self,
                          contentnode: addnodes.desc_content) -> None:
        name = self.arguments[0]
        # the source is always None here
        _, lineno = self.get_source_info()
        source = f'scylla_config:{lineno}:<{name}>'
        fields = StringList(self._render(name).splitlines(),
                            source=source, parent_offset=lineno)
        with switch_source_input(self.state, fields):
            self.state.nested_parse(fields, 0, contentnode)

    def add_target_and_index(self,
                             name: str,
                             sig: str,
                             signode: addnodes.desc_signature) -> None:
        node_id = make_id(self.env, self.state.document, self.objtype, name)
        signode['ids'].append(node_id)
        self.state.document.note_explicit_target(signode)
        entry = f'{name}; configuration option'
        self.indexnode['entries'].append(('pair', entry, node_id, '', None))
        std = self.env.get_domain('std')
        std.note_object(self.objtype, name, node_id, location=signode)


class ConfigOptionList(SphinxDirective):
    has_content = False
    required_arguments = 2
    optional_arguments = 0
    final_argument_whitespace = True
    option_spec = {
        'template': directives.path,
        'value_status': directives.unchanged_required,
    }

    @property
    def env(self):
        document = self.state.document
        return document.settings.env

    def _resolve_src_path(self, path: str) -> str:
        rel_filename, filename = self.env.relfn2path(path)
        self.env.note_dependency(filename)
        return filename

    def _render(self, context: Dict[str, Any]) -> str:
        builder = self.env.app.builder
        template = self.options.get('template')
        if template is None:
            self.error(f'Option "template" not specified!')
        return builder.templates.render(template, context)

    def _make_context(self) -> Dict[str, Any]:
        header = self._resolve_src_path(self.arguments[0])
        source = self._resolve_src_path(self.arguments[1])
        db_parser = DBConfigParser(source, header)
        value_status = self.options.get("value_status")
        return dict(data=db_parser.parse(),
                    value_status=value_status)

    def run(self) -> List[nodes.Node]:
        maybe_add_filters(self.env.app.builder)
        rendered = self._render(self._make_context())
        contents = StringList(rendered.splitlines())
        node = nodes.section()
        node.document = self.state.document
        nested_parse_with_titles(self.state, contents, node)
        return node.children


def setup(app: Sphinx) -> Dict[str, Any]:
    app.add_config_value(
        'scylladb_cc_properties_option_tmpl',
        default='db_option.tmpl',
        rebuild='html',
        types=[str])

    app.add_object_type(
        'confgroup',
        'confgroup',
        objname='configuration group',
        indextemplate='pair: %s; configuration group',
        doc_field_types=[
            Field('example',
                  label='Example',
                  has_arg=False)
        ])
    app.add_object_type(
        'confval',
        'confval',
        objname='configuration option')
    app.add_directive_to_domain('std', 'confval', ConfigOption, override=True)
    app.add_directive('scylladb_config_list', ConfigOptionList)

    return {
        "version": "0.1",
        "parallel_read_safe": True,
        "parallel_write_safe": True,
    }
