"""
Template Variable Parser for Plugin System

Parses template variables with formats:
- {{PROMPT:variable_name|description}} - User input
- {{PROMPT:variable_name|description|default}} - User input with default value
- {{PROMPT:variable_name|description|default|type}} - With explicit type
- {{PROMPT:variable_name|description||type}} - No default, explicit type

Supported types:
- text - Single-line text input (default)
- textarea - Multi-line text input
- email - Email address with validation
- number - Numeric input
- url - URL/webhook endpoint
- email_template - Dropdown to select from email templates

Other template formats:
- {{CONTEXT:field_name}} - Auto-filled business context
- {{DATE:expression}} - Date/time helpers

Insertion codes for use within PROMPT templates (e.g., email bodies):

Business context:
- {{BUSINESS_NAME}} - Business name
- {{BUSINESS_EMAIL}} - Business contact email
- {{BUSINESS_PHONE}} - Business phone number

Customer context:
- {{CUSTOMER_NAME}} - Customer's name
- {{CUSTOMER_EMAIL}} - Customer's email

Appointment context:
- {{APPOINTMENT_TIME}} - Appointment date/time
- {{APPOINTMENT_DATE}} - Appointment date only
- {{APPOINTMENT_SERVICE}} - Service name

Ticket context:
- {{TICKET_ID}} - Ticket ID number
- {{TICKET_SUBJECT}} - Ticket subject line
- {{TICKET_MESSAGE}} - Original ticket message
- {{TICKET_STATUS}} - Current ticket status
- {{TICKET_PRIORITY}} - Ticket priority level
- {{TICKET_CUSTOMER_NAME}} - Customer who created the ticket
- {{TICKET_URL}} - URL to view the ticket
- {{ASSIGNEE_NAME}} - Name of assigned staff member
- {{RECIPIENT_NAME}} - Name of email recipient
- {{REPLY_MESSAGE}} - Latest reply message
- {{RESOLUTION_MESSAGE}} - Resolution summary

Date/time helpers:
- {{TODAY}} - Today's date
- {{NOW}} - Current date and time
"""

import re
import html
from typing import Dict, List, Optional
from datetime import datetime, timedelta


class TemplateVariableParser:
    """Parse and process template variables"""

    # Pattern matches: {{PROMPT:variable_name|...}}
    # We'll extract the variable name and manually parse the content
    VARIABLE_PATTERN_START = r'\{\{PROMPT:([a-z_][a-z0-9_]*)\|'

    # Pattern for context variables: {{CONTEXT:field_name}}
    CONTEXT_PATTERN = r'\{\{CONTEXT:([a-z_][a-z0-9_]*)\}\}'

    # Pattern for date helpers: {{DATE:expression}}
    DATE_PATTERN = r'\{\{DATE:([^}]+)\}\}'

    # Pattern for insertion codes: {{BUSINESS_NAME}}, {{CUSTOMER_NAME}}, {{TICKET_ID}}, etc.
    INSERTION_PATTERN = r'\{\{(BUSINESS_NAME|BUSINESS_EMAIL|BUSINESS_PHONE|CUSTOMER_NAME|CUSTOMER_EMAIL|APPOINTMENT_TIME|APPOINTMENT_DATE|APPOINTMENT_SERVICE|TICKET_ID|TICKET_SUBJECT|TICKET_MESSAGE|TICKET_STATUS|TICKET_PRIORITY|TICKET_CUSTOMER_NAME|TICKET_URL|ASSIGNEE_NAME|RECIPIENT_NAME|REPLY_MESSAGE|RESOLUTION_MESSAGE|TODAY|NOW)\}\}'

    @classmethod
    def extract_variables(cls, template: str) -> List[Dict[str, str]]:
        """
        Extract all template variables from script.

        Args:
            template: Script containing {{PROMPT:var|description}} or {{PROMPT:var|description|default}} placeholders

        Returns:
            [
                {
                    'name': 'manager_email',
                    'label': 'Manager Email',
                    'description': 'The email address where reports will be sent',
                    'type': 'text',
                    'required': True,
                    'default': None,  # or default value if provided
                    'placeholder': 'Enter manager email'
                },
                ...
            ]
        """
        variables = []
        seen = set()

        # Find all PROMPT variable starts
        for match in re.finditer(cls.VARIABLE_PATTERN_START, template):
            var_name = match.group(1)

            # Skip duplicates (keep first occurrence)
            if var_name in seen:
                continue
            seen.add(var_name)

            # Find the matching }} by counting braces
            start_pos = match.end()
            brace_count = 1  # We're inside {{PROMPT:var|
            end_pos = start_pos

            while end_pos < len(template) - 1 and brace_count > 0:
                if template[end_pos:end_pos+2] == '{{':
                    brace_count += 1
                    end_pos += 2
                elif template[end_pos:end_pos+2] == '}}':
                    brace_count -= 1
                    if brace_count == 0:
                        break
                    end_pos += 2
                else:
                    end_pos += 1

            # Extract the content between var_name| and }}
            content = template[start_pos:end_pos]

            # Parse the content manually to handle nested {{ }}
            # Split by | but need to handle {{ }} pairs
            parts = cls._split_preserving_braces(content)

            description = parts[0].strip() if len(parts) > 0 else var_name
            default_value = parts[1].strip() if len(parts) > 1 and parts[1].strip() else None
            # Explicit type can be in parts[2] or parts[3] depending on whether default is provided
            # Format: description|default|type or description||type
            explicit_type = None
            if len(parts) > 3 and parts[3].strip():
                explicit_type = parts[3].strip()
            elif len(parts) > 2 and parts[2].strip():
                explicit_type = parts[2].strip()

            label = cls._variable_to_label(var_name)

            # Use explicit type if provided, otherwise infer
            # Supported types: text, textarea, email, number, url, email_template
            if explicit_type and explicit_type.strip().lower() in ['text', 'textarea', 'email', 'number', 'url', 'email_template']:
                var_type = explicit_type.strip().lower()
            else:
                var_type = cls._infer_type(var_name, description)

            variables.append({
                'name': var_name,
                'label': label,
                'description': description.strip(),
                'type': var_type,
                'required': not bool(default_value),  # Optional if has default
                'default': default_value.strip() if default_value else None,
                'placeholder': cls._generate_placeholder(label, var_type)
            })

        return variables

    @classmethod
    def _split_preserving_braces(cls, text: str) -> List[str]:
        """
        Split text by | while preserving {{ }} pairs.

        Example: "Email Message|Hi {{NAME}}||textarea" -> ["Email Message", "Hi {{NAME}}", "", "textarea"]
        """
        parts = []
        current = []
        brace_depth = 0
        i = 0

        while i < len(text):
            if i < len(text) - 1 and text[i:i+2] == '{{':
                brace_depth += 1
                current.append(text[i:i+2])
                i += 2
            elif i < len(text) - 1 and text[i:i+2] == '}}':
                brace_depth -= 1
                current.append(text[i:i+2])
                i += 2
            elif text[i] == '|' and brace_depth == 0:
                # Split here
                parts.append(''.join(current))
                current = []
                i += 1
            else:
                current.append(text[i])
                i += 1

        # Add final part
        if current or (text and text[-1] == '|'):
            parts.append(''.join(current))

        return parts

    @classmethod
    def _variable_to_label(cls, var_name: str) -> str:
        """Convert snake_case to Title Case"""
        return var_name.replace('_', ' ').title()

    @classmethod
    def _infer_type(cls, var_name: str, description: str) -> str:
        """
        Infer input type from variable name and description.

        Returns: 'email', 'number', 'url', 'textarea', or 'text'
        """
        lower_name = var_name.lower()
        lower_desc = description.lower()

        # Number detection (check first to avoid conflicts)
        if any(word in lower_name for word in ['count', 'number', 'days', 'hours', 'minutes', 'amount', 'limit', 'threshold']):
            return 'number'
        if any(word in lower_desc for word in ['number of', 'how many', 'count', 'minimum', 'maximum']):
            return 'number'

        # Email detection (strict - must have 'email' keyword)
        if 'email' in lower_name and 'address' not in lower_desc:
            return 'email'
        if 'email address' in lower_desc:
            return 'email'

        # URL detection
        if 'url' in lower_name or 'webhook' in lower_name or 'endpoint' in lower_name:
            return 'url'
        if 'webhook' in lower_desc or 'endpoint' in lower_desc:
            return 'url'

        # Textarea for longer content
        if any(word in lower_name for word in ['message', 'body', 'content', 'description']):
            return 'textarea'

        # Default to text
        return 'text'

    @classmethod
    def _generate_placeholder(cls, label: str, field_type: str) -> str:
        """Generate appropriate placeholder text"""
        if field_type == 'email':
            return f'user@example.com'
        elif field_type == 'number':
            return f'Enter {label.lower()}'
        elif field_type == 'url':
            return 'https://example.com/webhook'
        elif field_type == 'textarea':
            return f'Enter {label.lower()}'
        else:
            return f'Enter {label.lower()}'

    @classmethod
    def compile_template(cls, template: str, config_values: Dict[str, str], context: Optional[Dict[str, str]] = None) -> str:
        """
        Replace template variables with actual values.

        Args:
            template: Script with {{PROMPT:variable|description}}, {{CONTEXT:field}}, {{DATE:expr}} placeholders
            config_values: {variable_name: user_value}
            context: Business context data {business_name, owner_email, owner_name, etc.}

        Returns:
            Compiled script with values substituted

        Raises:
            ValueError: If required variables are missing or contains HTML
        """
        compiled = template

        # 1. Replace PROMPT variables
        def replace_var(match):
            var_name = match.group(1)
            # description = match.group(2)  # Not needed for compilation
            default_value = match.group(3) if len(match.groups()) > 2 else None

            value = config_values.get(var_name)

            # Use default if value not provided
            if (value is None or value == '') and default_value:
                value = default_value

            if value is None or value == '':
                raise ValueError(f"Missing required configuration: {var_name}")

            # Sanitize: Check for HTML tags (but allow insertion codes)
            if cls._contains_html_excluding_insertions(value):
                raise ValueError(f"HTML not allowed in configuration field: {var_name}")

            # Process insertion codes within the value before escaping
            # This allows users to use {{BUSINESS_NAME}}, {{CUSTOMER_NAME}}, etc. in their text
            processed_value = cls._mark_insertions_for_runtime(str(value))

            # Escape the value for Python string safety
            # This ensures strings are properly quoted in the compiled script
            return repr(processed_value)

        compiled = re.sub(cls.VARIABLE_PATTERN, replace_var, compiled)

        # 2. Replace CONTEXT variables
        if context:
            def replace_context(match):
                field_name = match.group(1)
                value = context.get(field_name)

                if value is None:
                    raise ValueError(f"Context field not available: {field_name}")

                return repr(str(value))

            compiled = re.sub(cls.CONTEXT_PATTERN, replace_context, compiled)

        # 3. Replace DATE helpers
        def replace_date(match):
            expression = match.group(1).strip().lower()
            date_value = cls._evaluate_date_expression(expression)
            return repr(date_value)

        compiled = re.sub(cls.DATE_PATTERN, replace_date, compiled)

        return compiled

    @classmethod
    def _evaluate_date_expression(cls, expression: str) -> str:
        """
        Evaluate date/time expressions.

        Supported formats:
        - today, now
        - tomorrow, yesterday
        - +Nd, -Nd (N days from/before today)
        - +Nw, -Nw (N weeks)
        - monday, tuesday, etc. (next occurrence)

        Returns ISO format date string: YYYY-MM-DD
        """
        now = datetime.now()

        # Today/Now
        if expression in ['today', 'now']:
            return now.strftime('%Y-%m-%d')

        # Tomorrow/Yesterday
        if expression == 'tomorrow':
            return (now + timedelta(days=1)).strftime('%Y-%m-%d')
        if expression == 'yesterday':
            return (now - timedelta(days=1)).strftime('%Y-%m-%d')

        # Relative days: +7d, -30d
        match = re.match(r'([+-])(\d+)d', expression)
        if match:
            sign, days = match.groups()
            delta = int(days) if sign == '+' else -int(days)
            return (now + timedelta(days=delta)).strftime('%Y-%m-%d')

        # Relative weeks: +2w, -1w
        match = re.match(r'([+-])(\d+)w', expression)
        if match:
            sign, weeks = match.groups()
            delta = int(weeks) * 7 if sign == '+' else -int(weeks) * 7
            return (now + timedelta(days=delta)).strftime('%Y-%m-%d')

        # Next weekday: monday, tuesday, etc.
        weekdays = {
            'monday': 0, 'tuesday': 1, 'wednesday': 2, 'thursday': 3,
            'friday': 4, 'saturday': 5, 'sunday': 6
        }
        if expression in weekdays:
            target_weekday = weekdays[expression]
            current_weekday = now.weekday()
            days_ahead = (target_weekday - current_weekday) % 7
            if days_ahead == 0:  # If today is the target day, get next week
                days_ahead = 7
            return (now + timedelta(days=days_ahead)).strftime('%Y-%m-%d')

        # Default: return today
        return now.strftime('%Y-%m-%d')

    @classmethod
    def validate_config(cls, template: str, config_values: Dict[str, str]) -> List[str]:
        """
        Validate that all required variables are provided.

        Args:
            template: Script template
            config_values: User-provided values

        Returns:
            List of error messages (empty if valid)
        """
        required_vars = cls.extract_variables(template)
        errors = []

        for var in required_vars:
            var_name = var['name']
            value = config_values.get(var_name)

            # Check if required field is empty
            if not value or str(value).strip() == '':
                errors.append(f"{var['label']} is required")
                continue

            # Check for HTML tags (security)
            if cls._contains_html(str(value)):
                errors.append(f"{var['label']} cannot contain HTML tags")
                continue

            # Type-specific validation
            if var['type'] == 'email':
                if not cls._is_valid_email(value):
                    errors.append(f"{var['label']} must be a valid email address")

            elif var['type'] == 'number':
                if not cls._is_valid_number(value):
                    errors.append(f"{var['label']} must be a valid number")

            elif var['type'] == 'url':
                if not cls._is_valid_url(value):
                    errors.append(f"{var['label']} must be a valid URL")

        return errors

    @classmethod
    def _contains_html(cls, value: str) -> bool:
        """
        Check if string contains HTML tags.

        Returns True if HTML tags are detected.
        """
        # Check for common HTML tags
        html_pattern = r'<[^>]+>'
        return bool(re.search(html_pattern, str(value)))

    @classmethod
    def _contains_html_excluding_insertions(cls, value: str) -> bool:
        """
        Check if string contains HTML tags, but allow insertion codes like {{BUSINESS_NAME}}.

        Returns True if HTML tags are detected (excluding insertion codes).
        """
        # Temporarily replace insertion codes with placeholders
        temp_value = re.sub(cls.INSERTION_PATTERN, '___INSERTION___', str(value))

        # Check for common HTML tags in the modified string
        html_pattern = r'<[^>]+>'
        return bool(re.search(html_pattern, temp_value))

    @classmethod
    def _mark_insertions_for_runtime(cls, value: str) -> str:
        """
        Mark insertion codes to be processed at runtime via f-string interpolation.

        Converts {{BUSINESS_NAME}} to {business_name}, {{CUSTOMER_NAME}} to {customer_name}, etc.
        These will be filled in by the SafeScriptAPI at runtime.

        Args:
            value: String containing insertion codes

        Returns:
            String with insertion codes converted to Python f-string format
        """
        # Map insertion codes to runtime variable names
        insertion_map = {
            # Business context
            'BUSINESS_NAME': '{business_name}',
            'BUSINESS_EMAIL': '{business_email}',
            'BUSINESS_PHONE': '{business_phone}',
            # Customer context
            'CUSTOMER_NAME': '{customer_name}',
            'CUSTOMER_EMAIL': '{customer_email}',
            # Appointment context
            'APPOINTMENT_TIME': '{appointment_time}',
            'APPOINTMENT_DATE': '{appointment_date}',
            'APPOINTMENT_SERVICE': '{appointment_service}',
            # Ticket context
            'TICKET_ID': '{ticket_id}',
            'TICKET_SUBJECT': '{ticket_subject}',
            'TICKET_MESSAGE': '{ticket_message}',
            'TICKET_STATUS': '{ticket_status}',
            'TICKET_PRIORITY': '{ticket_priority}',
            'TICKET_CUSTOMER_NAME': '{ticket_customer_name}',
            'TICKET_URL': '{ticket_url}',
            'ASSIGNEE_NAME': '{assignee_name}',
            'RECIPIENT_NAME': '{recipient_name}',
            'REPLY_MESSAGE': '{reply_message}',
            'RESOLUTION_MESSAGE': '{resolution_message}',
            # Date/time helpers
            'TODAY': '{today}',
            'NOW': '{now}',
        }

        def replace_insertion(match):
            code = match.group(1)
            return insertion_map.get(code, match.group(0))

        return re.sub(cls.INSERTION_PATTERN, replace_insertion, value)

    @classmethod
    def _is_valid_email(cls, value: str) -> bool:
        """Basic email validation"""
        email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return bool(re.match(email_pattern, str(value)))

    @classmethod
    def _is_valid_number(cls, value: str) -> bool:
        """Check if value is a valid number"""
        try:
            float(str(value))
            return True
        except (ValueError, TypeError):
            return False

    @classmethod
    def _is_valid_url(cls, value: str) -> bool:
        """Basic URL validation"""
        url_pattern = r'^https?://[^\s/$.?#].[^\s]*$'
        return bool(re.match(url_pattern, str(value)))

    @classmethod
    def get_config_schema(cls, template: str) -> Dict[str, Dict]:
        """
        Generate JSON schema for configuration form.

        Returns:
            {
                'variable_name': {
                    'label': 'Variable Name',
                    'description': 'Human-readable description',
                    'type': 'email',
                    'required': True,
                    'placeholder': 'user@example.com'
                },
                ...
            }
        """
        variables = cls.extract_variables(template)
        schema = {}

        for var in variables:
            schema[var['name']] = {
                'label': var['label'],
                'description': var['description'],
                'type': var['type'],
                'required': var['required'],
                'placeholder': var['placeholder']
            }

        return schema


# Example usage and testing
if __name__ == '__main__':
    # Example template
    template = """
from datetime import datetime, timedelta

# Get inactive customers
cutoff_days = {{PROMPT:days_inactive|Number of days before a customer is considered inactive}}
customers = api.get_customers(has_email=True)

# Send re-engagement emails
for customer in customers[:30]:
    api.send_email(
        to=customer['email'],
        subject={{PROMPT:email_subject|Subject line for the re-engagement email}},
        body=f'''Hi {customer['name']},

We miss you! Get {{PROMPT:discount_code|Promotional discount code to offer}} off your next visit.

Best regards,
{{PROMPT:business_name|Your business name}}'''
    )

result = {'sent': len(customers)}
"""

    # Extract variables
    parser = TemplateVariableParser()
    variables = parser.extract_variables(template)

    print("Extracted Variables:")
    for var in variables:
        print(f"  {var['name']}: {var['label']}")
        print(f"    Description: {var['description']}")
        print(f"    Type: {var['type']}")
        print()

    # Test compilation
    config = {
        'days_inactive': '60',
        'email_subject': 'We Miss You!',
        'discount_code': 'COMEBACK20',
        'business_name': 'Acme Spa'
    }

    compiled = parser.compile_template(template, config)
    print("Compiled Script:")
    print(compiled)

    # Test validation
    incomplete_config = {
        'days_inactive': '60',
        'email_subject': 'We Miss You!'
        # Missing: discount_code, business_name
    }

    errors = parser.validate_config(template, incomplete_config)
    print("\nValidation Errors:")
    for error in errors:
        print(f"  - {error}")
