from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Union

from .pit_constants import (
    PITConstants,
    get_datetable_defaults,
    validate_datetable_params,
)
from .pit_enums import *

# Core expression classes (shared base - no circular dependency)
from .pit_formula_base import (
    BRACELESS_KEYWORDS,
    BinaryOp,
    FormulaExpression,
    FunctionCall,
    Literal,
    UnaryOp,
    _wrap,
    is_braceless_keyword,
)

# Generated keywords mixin (imports from pit_formula_base, NOT from this module)
from .pit_keywords import PITKeywordsMixin

# Control what gets imported with "from priceit.sdk.pit_formula import *"
__all__ = [
    # Core formula expression classes
    "FormulaExpression",
    "Literal",
    "FunctionCall",
    "BinaryOp",
    "UnaryOp",
    # DateTable
    "DateTable",
    # Main formula builder
    "F",
    # Registry and utilities
    "ScriptFunctionRegistry",
    "ScriptFunction",
]

# ==================== DateTable ====================


class DateTable(FunctionCall):
    """
    Represents a DateTable function call with validation and named parameter support.

    DateTable generates a schedule of dates based on start/end dates and various parameters.

    Parameters:
        startDate (required): Start date of the schedule
        endDate (required): End date of the schedule
        Frequency (optional): Payment frequency (e.g., "3M", "6M", "1Y")
        DayCount (optional): Day count convention (e.g., "Actual/360", "30/360")
        ResetGap (optional): Reset gap in days (e.g., "-2b", "0b")
        PayGap (optional): Payment gap in days (e.g., "0b", "5d")
        Calendar (optional): Calendar name (e.g., "TARGET", "USD", "London")
        ResetTiming (optional): Reset timing ("Advance" or "Arrears")
        Stub (optional): Stub rule ("ShortStart", "LongStart", "ShortEnd", "LongEnd")
        DateRule (optional): Date adjustment rule (e.g., "ModifiedFollowing", "Unadjusted")
        PaymentDateRule (optional): Payment date adjustment rule (same as DateRule)
        PayTiming (optional): Payment timing ("Advance" or "Arrears")
        RefDate (optional): Reference date
    """

    # Parameter names in signature order
    PARAM_NAMES = [
        "StartDate",  # [0] Required
        "EndDate",  # [1] Required
        "Freq",  # [2] Optional
        "DayCount",  # [3] Optional
        "ResetGap",  # [4] Optional
        "PayGap",  # [5] Optional
        "Calendar",  # [6] Optional
        "ResetTiming",  # [7] Optional
        "Stub",  # [8] Optional
        "DateRule",  # [9] Optional
        "PaymentDateRule",  # [10] Optional
        "PayTiming",  # [11] Optional
        "RefDate",  # [12] Optional
    ]

    def __init__(
        self,
        startDate: Union[str, FormulaExpression],
        endDate: Union[str, FormulaExpression],
        *args,
        Frequency: Optional[str] = None,
        Freq: Optional[str] = None,
        DayCount: Optional[str] = None,
        ResetGap: Optional[str] = None,
        PayGap: Optional[str] = None,
        Calendar: Optional[str] = None,
        ResetTiming: Optional[str] = None,
        Stub: Optional[str] = None,
        DateRule: Optional[str] = None,
        PaymentDateRule: Optional[str] = None,
        PayTiming: Optional[str] = None,
        RefDate: Optional[str] = None,
        validate: bool = True,
    ):
        self.func_name = "DateTable"
        self.startDate = _wrap(startDate)
        self.endDate = _wrap(endDate)
        self.named_params: Dict[str, str] = {}

        # Handle positional optional parameters
        if args:
            for i, arg in enumerate(args):
                if i < len(self.PARAM_NAMES) - 2:
                    self.named_params[self.PARAM_NAMES[i + 2]] = str(arg)

        # Handle named parameters (override positional if provided)
        if Frequency is not None:
            self.named_params["Freq"] = Frequency
        if Freq is not None:
            self.named_params["Freq"] = Freq
        if DayCount is not None:
            self.named_params["DayCount"] = DayCount
        if ResetGap is not None:
            self.named_params["ResetGap"] = ResetGap
        if PayGap is not None:
            self.named_params["PayGap"] = PayGap
        if Calendar is not None:
            self.named_params["Calendar"] = Calendar
        if ResetTiming is not None:
            self.named_params["ResetTiming"] = ResetTiming
        if Stub is not None:
            self.named_params["Stub"] = Stub
        if DateRule is not None:
            self.named_params["DateRule"] = DateRule
        if PaymentDateRule is not None:
            self.named_params["PaymentDateRule"] = PaymentDateRule
        if PayTiming is not None:
            self.named_params["PayTiming"] = PayTiming
        if RefDate is not None:
            self.named_params["RefDate"] = RefDate

        if validate and self.named_params:
            self._validate_parameters()

        self.args = [self.startDate, self.endDate]

    def _validate_parameters(self) -> None:
        """Validate parameter values against enum constants."""
        params_for_validation = self.named_params.copy()
        if "Freq" in params_for_validation:
            params_for_validation["Frequency"] = params_for_validation.pop("Freq")

        validation_results = validate_datetable_params(params_for_validation)

        invalid_params = [
            (name, value)
            for name, value in self.named_params.items()
            if ("Frequency" if name == "Freq" else name) in validation_results
            and not validation_results["Frequency" if name == "Freq" else name]
        ]

        if invalid_params:
            errors = [f"{name}='{value}'" for name, value in invalid_params]
            raise ValueError(f"Invalid DateTable parameters: {', '.join(errors)}")

    def to_string(self, use_named_syntax: bool = True) -> str:
        """Convert DateTable to PIT string representation."""
        parts = [self.startDate.to_string(), self.endDate.to_string()]

        if use_named_syntax and self.named_params:
            for name, value in self.named_params.items():
                parts.append(f"{name}:={value}")
        else:
            for param_name in self.PARAM_NAMES[2:]:
                if param_name in self.named_params:
                    parts.append(self.named_params[param_name])

        return f"DateTable({', '.join(parts)})"

    def get_parameter(self, name: str) -> Optional[str]:
        """Get the value of a named parameter."""
        if name == "Frequency":
            name = "Freq"
        return self.named_params.get(name)

    def set_parameter(self, name: str, value: str, validate: bool = True) -> None:
        """Set a named parameter value."""
        if name == "Frequency":
            name = "Freq"
        if name not in self.PARAM_NAMES[2:]:
            raise ValueError(f"Invalid parameter name: {name}")
        if validate:
            validation_name = "Frequency" if name == "Freq" else name
            if not validate_datetable_params({validation_name: value}).get(validation_name, True):
                raise ValueError(f"Invalid value for {name}: {value}")
        self.named_params[name] = value

    def get_all_parameters(self) -> Dict[str, str]:
        """Get all named parameters as a dictionary."""
        return self.named_params.copy()

    @classmethod
    def with_defaults(
        cls, startDate: Union[str, FormulaExpression], endDate: Union[str, FormulaExpression], **overrides
    ) -> "DateTable":
        """Create DateTable with default values for all optional parameters."""
        defaults = get_datetable_defaults()
        defaults.update(overrides)
        return cls(startDate, endDate, **defaults)

    @classmethod
    def get_parameter_info(cls) -> Dict[str, Dict[str, Any]]:
        """Get information about all parameters."""
        return {
            "StartDate": {"position": 0, "required": True, "type": "date", "description": "Start date of the schedule"},
            "EndDate": {"position": 1, "required": True, "type": "date", "description": "End date of the schedule"},
            "Freq": {
                "position": 2,
                "required": False,
                "type": "enum",
                "enum_type": "Freq::Type",
                "default": PITConstants.get_frequency_default(),
                "values": PITConstants.get_frequency_values(),
                "description": "Payment frequency",
            },
            "DayCount": {
                "position": 3,
                "required": False,
                "type": "enum",
                "enum_type": "DayCount::Type",
                "default": PITConstants.get_daycount_default(),
                "values": PITConstants.get_daycount_values(),
                "description": "Day count convention",
            },
            "ResetGap": {
                "position": 4,
                "required": False,
                "type": "string",
                "format": "gap",
                "default": "DEFAULT",
                "description": "Reset gap (e.g., '-2b', '0b')",
            },
            "PayGap": {
                "position": 5,
                "required": False,
                "type": "string",
                "format": "gap",
                "default": "DEFAULT",
                "description": "Payment gap (e.g., '0b', '5d')",
            },
            "Calendar": {
                "position": 6,
                "required": False,
                "type": "enum",
                "enum_type": "Calendar",
                "default": PITConstants.get_calendar_default(),
                "values": PITConstants.get_calendar_values(),
                "common_values": PITConstants.get_calendar_common(),
                "description": "Calendar name",
            },
            "ResetTiming": {
                "position": 7,
                "required": False,
                "type": "enum",
                "enum_type": "ResetTiming::Type",
                "default": PITConstants.get_resettiming_default(),
                "values": PITConstants.get_resettiming_values(),
                "description": "Reset timing (Advance/Arrears)",
            },
            "Stub": {
                "position": 8,
                "required": False,
                "type": "enum",
                "enum_type": "StubRule::Type",
                "default": PITConstants.get_stub_default(),
                "values": PITConstants.get_stub_values(),
                "description": "Stub rule",
            },
            "DateRule": {
                "position": 9,
                "required": False,
                "type": "enum",
                "enum_type": "DateAdjRule::Type",
                "default": PITConstants.get_daterule_default(),
                "values": PITConstants.get_daterule_values(),
                "description": "Date adjustment rule",
            },
            "PaymentDateRule": {
                "position": 10,
                "required": False,
                "type": "enum",
                "enum_type": "DateAdjRule::Type",
                "default": PITConstants.get_paymentdaterule_default(),
                "values": PITConstants.get_paymentdaterule_values(),
                "description": "Payment date adjustment rule",
            },
            "PayTiming": {
                "position": 11,
                "required": False,
                "type": "enum",
                "enum_type": "ResetTiming::Type",
                "default": PITConstants.get_paytiming_default(),
                "values": PITConstants.get_paytiming_values(),
                "description": "Payment timing (Advance/Arrears)",
            },
            "RefDate": {"position": 12, "required": False, "type": "date", "description": "Reference date"},
        }


# ==================== Formula Builder (F) ====================


class F(PITKeywordsMixin):
    """
    Formula builder - short name for convenience.

    PIT Keywords (inherited from PITKeywordsMixin - SINGLE SOURCE):
        F.eq_spot('SX5E')       # Equity spot
        F.fx_spot('EURUSD')     # FX spot
        F.dcf(from_, to_, ...)  # Day count fraction
        F.if_(cond, t, f)       # If-then-else (PEP 8 trailing underscore)
        ... and more

    Other methods (defined here):
        F.receive(amount)       # Receive payment
        F.pay(amount)           # Make payment
        F.max(*values)          # Maximum - NOTE: overrides mixin for variadic args
        F.min(*values)          # Minimum - NOTE: overrides mixin for variadic args
        F.date_table(...)       # DateTable creation
    """

    # variables/constants

    t = Literal("t")

    # ===== Methods that are NOT in the generated mixin =====
    # These are specific to F or have different signatures

    @staticmethod
    def interest_term() -> FunctionCall:
        """Compute the interest term over the current period"""
        return FunctionCall("InterestTerm")

    @staticmethod
    def as_of_date() -> FunctionCall:
        """Retrieve the as of date"""
        return FunctionCall("AsOfDate")

    @staticmethod
    def receive(amount: Union[FormulaExpression, str, int, float]) -> FunctionCall:
        """Receive payment"""
        return FunctionCall("Receive", amount, _braceless=True)

    @staticmethod
    def pay(amount: Union[FormulaExpression, str, int, float]) -> FunctionCall:
        """Make payment"""
        return FunctionCall("Pay", amount, _braceless=True)

    @staticmethod
    def column_val(amount: Union[FormulaExpression, str, int, float]) -> FunctionCall:
        """Assign value to column"""
        return FunctionCall("ColumnVal", amount)

    @staticmethod
    def report(value: Union[FormulaExpression, str, int, float]) -> FunctionCall:
        """Report a value"""
        return FunctionCall("Report", value)

    # NOTE: These override the mixin versions to support variadic arguments
    @staticmethod
    def max(*values: Union[FormulaExpression, str, int, float]) -> FunctionCall:
        """Maximum of values (variadic)"""
        return FunctionCall("Max", *values)

    @staticmethod
    def min(*values: Union[FormulaExpression, str, int, float]) -> FunctionCall:
        """Minimum of values (variadic)"""
        return FunctionCall("Min", *values)

    @staticmethod
    def abs(value: Union[FormulaExpression, str, int, float]) -> FunctionCall:
        """Absolute value"""
        return FunctionCall("Abs", value)

    # Date/Schedule functions
    @staticmethod
    def date_table(
        start_date: Union[str, FormulaExpression],
        end_date: Union[str, FormulaExpression],
        Frequency: Optional[Frequency] = None,
        Freq: Optional[Frequency] = None,
        DayCount: Optional[DayCount] = None,
        ResetGap: Optional[str] = None,
        PayGap: Optional[str] = None,
        Calendar: Optional[Calendar] = None,
        ResetTiming: Optional[ResetTiming] = None,
        Stub: Optional[Stub] = None,
        DateRule: Optional[DateRule] = None,
        PaymentDateRule: Optional[DateRule] = None,
        PayTiming: Optional[PayTiming] = None,
        RefDate: Optional[str] = None,
    ) -> FunctionCall:
        """
        Generate a table of dates between start and end.

        Args:
            start_date: Start date
            end_date: End date
            Frequency: Payment frequency (e.g., Frequency.QUARTERLY or "3M")
            DayCount: Day count convention (e.g., DayCount.ACTUAL_360 or "Actual/360")
            ResetGap: Reset gap (e.g., "-2b", "0b", "DEFAULT")
            PayGap: Payment gap (e.g., "0b", "5d", "DEFAULT")
            Calendar: Calendar name (e.g., Calendar.TARGET or "TARGET")
            ResetTiming: Reset timing (ResetTiming.ADVANCE or "Advance")
            Stub: Stub rule (e.g., Stub.SHORT_START or "ShortStart")
            DateRule: Date adjustment rule (e.g., DateRule.MODIFIED_FOLLOWING or "ModifiedFollowing")
            PaymentDateRule: Payment date adjustment rule (same as DateRule)
            PayTiming: Payment timing (PayTiming.ARREARS or "Arrears")
            RefDate: Reference date

        Example:
            F.date_table("StartDate", "EndDate", Frequency=Frequency.QUARTERLY, Calendar=Calendar.TARGET)
            F.date_table("StartDate", "EndDate", Frequency="3M", Calendar="TARGET")
        """
        # Build kwargs dict for forwarding to DateTable
        # Convert enum values to strings
        effective_frequency = Frequency if Frequency is not None else Freq

        kwargs = {}
        if effective_frequency is not None:
            kwargs["Frequency"] = str(effective_frequency)
        if DayCount is not None:
            kwargs["DayCount"] = str(DayCount)
        if ResetGap is not None:
            kwargs["ResetGap"] = ResetGap
        if PayGap is not None:
            kwargs["PayGap"] = PayGap
        if Calendar is not None:
            kwargs["Calendar"] = str(Calendar)
        if ResetTiming is not None:
            kwargs["ResetTiming"] = str(ResetTiming)
        if Stub is not None:
            kwargs["Stub"] = str(Stub)
        if DateRule is not None:
            kwargs["DateRule"] = str(DateRule)
        if PaymentDateRule is not None:
            kwargs["PaymentDateRule"] = str(PaymentDateRule)
        if PayTiming is not None:
            kwargs["PayTiming"] = str(PayTiming)
        if RefDate is not None:
            kwargs["RefDate"] = RefDate

        if kwargs:
            # Skip enum-value validation when optional params are formula/input references
            # (e.g., payoff.add_input("FixedFrequency", ...) used in DateTable)
            ref_params = [
                effective_frequency,
                DayCount,
                Calendar,
                ResetTiming,
                Stub,
                DateRule,
                PaymentDateRule,
                PayTiming,
            ]
            has_formula_refs = any(isinstance(param, FormulaExpression) for param in ref_params if param is not None)
            return DateTable(start_date, end_date, validate=not has_formula_refs, **kwargs)

        return FunctionCall("DateTable", start_date, end_date)

    @staticmethod
    def date_list(*dates: Union[str, FormulaExpression]) -> FunctionCall:
        """Create a list of dates"""
        return FunctionCall("DateList", *dates)

    @staticmethod
    def add_days(date: Union[str, FormulaExpression], days: Union[int, FormulaExpression]) -> FunctionCall:
        """Add days to a date"""
        return FunctionCall("AddDays", date, days)

    @staticmethod
    def add_months(date: Union[str, FormulaExpression], months: Union[int, FormulaExpression]) -> FunctionCall:
        """Add months to a date"""
        return FunctionCall("AddMonths", date, months)

    @staticmethod
    def add_years(date: Union[str, FormulaExpression], years: Union[int, FormulaExpression]) -> FunctionCall:
        """Add years to a date"""
        return FunctionCall("AddYears", date, years)

    @staticmethod
    def business_days(
        start_date: Union[str, FormulaExpression],
        end_date: Union[str, FormulaExpression],
        calendar: Optional[Union[str, FormulaExpression]] = None,
    ) -> FunctionCall:
        """Generate business days between start and end"""
        if calendar:
            return FunctionCall("BusinessDays", start_date, end_date, calendar)
        return FunctionCall("BusinessDays", start_date, end_date)

    @staticmethod
    def lit(value: Union[int, float, str]) -> Literal:
        """Create a literal value"""
        return Literal(value)

    # PITL Global Variable References (with $ prefix)
    @staticmethod
    def global_var(name: str) -> Literal:
        """
        Create a PITL global variable reference.

        PITL global variables are identifiers that start with $ (e.g., $n, $n1, $counter).
        These are global variables that persist across schedule periods in PITL.
        This is distinct from deal inputs in PITL.

        Args:
            name: Variable name without the $ prefix

        Returns:
            Literal that generates $name in PITL

        Example:
            F.global_var("n")       # generates "$n"
            F.global_var("counter") # generates "$counter"
        """
        return Literal(f"${name}")

    @staticmethod
    def pit_statements(*statements: Union[FormulaExpression, str]) -> Literal:
        """
        Join multiple PITL statements with semicolons.

            The plural name indicates this requires at least 2 statements.
            For a single statement, use the expression directly.

            Args:
                *statements: Two or more statements to join with semicolons

        Returns:
            Literal containing statements separated by semicolons

        Raises:
            ValueError: If fewer than 2 statements provided

        Example:
            F.pit_statements(
                F.global_var("n") + 1,
                F.global_var("sum") + F.global_var("n"),
                F.global_var("count") + 1
            )
            # Generates: "$n + 1; $sum + $n; $count + 1"
        """
        if len(statements) < 2:
            raise ValueError(
                "pit_statements requires at least 2 statements. " "For a single statement, use the expression directly."
            )

        parts = []
        for stmt in statements:
            if isinstance(stmt, FormulaExpression):
                parts.append(stmt.to_string())
            else:
                parts.append(str(stmt))

        return Literal("; ".join(parts))

    @staticmethod
    def statements(*statements: Union[FormulaExpression, str]) -> Literal:
        """
        Synonym for pit_statements(). Join multiple PITL statements with semicolons.

        The plural name indicates this requires at least 2 statements.
        For a single statement, use the expression directly.

        Args:
            *statements: Two or more statements to join with semicolons

        Returns:
            Literal containing statements separated by semicolons

        Raises:
            ValueError: If fewer than 2 statements provided

        Example:
            F.statements(
                F.global_var("n") + 1,
                F.global_var("sum") + F.global_var("n"),
                F.global_var("count") + 1
            )
            # Generates: "$n + 1; $sum + $n; $count + 1"

        Note:
            This is an alias for pit_statements() - both work identically.
            Use whichever name you prefer for readability.
        """
        if len(statements) < 2:
            raise ValueError(
                "statements requires at least 2 statements. " "For a single statement, use the expression directly."
            )

        parts = []
        for stmt in statements:
            if isinstance(stmt, FormulaExpression):
                parts.append(stmt.to_string())
            else:
                parts.append(str(stmt))

        return Literal("; ".join(parts))

    @staticmethod
    def assign(variable: Union[str, FormulaExpression], value: Union[str, int, float, FormulaExpression]) -> Literal:
        """
        Create a PITL assignment statement.

        Generates: variable = value

        This helper enables assignment syntax since Python doesn't allow
        variable = expression in function arguments (interprets as keyword arg).

        Args:
            variable: Variable name or expression to assign to
            value: Value to assign

        Returns:
            Literal containing the assignment statement

        Example:
            # Simple assignment
            F.assign("counter", 0)
            # Generates: "counter = 0"

            # With PITL variable
            F.assign(F.global_var("n"), F.global_var("n") + 1)
            # Generates: "$n = $n + 1"

            # With conditional
            F.assign("result", F.if_(rate > cap, cap, rate))
            # Generates: "result = If(rate > cap, cap, rate)"

            # In pit_statements
            F.pit_statements(
                F.assign("counter", F.if_(rate > barrier, counter + 1, counter)),
                F.assign("result", counter * 2)
            )
            # Generates: "counter = If(rate > barrier, counter + 1, counter); result = counter * 2"
        """
        # Convert variable to string
        if isinstance(variable, FormulaExpression):
            var_str = variable.to_string()
        else:
            var_str = str(variable)

        # Convert value to string
        if isinstance(value, FormulaExpression):
            val_str = value.to_string()
        else:
            val_str = str(value)

        return Literal(f"{var_str} = {val_str}")


# ==================== Script Function Registry ====================


class ScriptFunctionRegistry:
    FUNCTIONS = {}

    @classmethod
    def register(cls, name, description, example, params):
        def decorator(func):
            cls.FUNCTIONS[name] = {"func": func, "description": description, "example": example, "params": params}
            return func

        return decorator


# Replace ScriptFunction class with a display helper
class ScriptFunction:
    @classmethod
    def list_functions(cls) -> str:
        lines = ["Available Script Functions:", "=" * 50]
        for func_name, info in ScriptFunctionRegistry.FUNCTIONS.items():
            params = ", ".join(info["params"])
            lines.append(f"\n{func_name}({params})")
            lines.append(f"  Description: {info['description']}")
            lines.append(f"  Example: {info['example']}")
        return "\n".join(lines)
