# source: https://raw.githubusercontent.com/Or-Harari/OGAYN/c902c8e90a7b2ed4cd91779b0da06a2bb2582d3a/backend/app/services/strategy_file_service.py
# directory_url: https://github.com/Or-Harari/OGAYN/blob/main/backend/app/services/
# User: Or-Harari
# Repository: OGAYN
# --------------------"""
Strategy File Service
Handles safe file operations for user strategy files.

Security:
- All paths resolved relative to user's strategies directory
- No path traversal allowed
- Only .py files
- Syntax validation before save
- Name validation (Python identifier rules)
"""
from __future__ import annotations

import ast
import keyword
import os
import re
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional

from sqlalchemy.orm import Session

from ..db.models import StrategyComponent, StrategyParam

# Maximum file size: 1MB
MAX_STRATEGY_FILE_SIZE = 1024 * 1024

# Valid strategy name pattern
STRATEGY_NAME_PATTERN = re.compile(r'^[A-Za-z][A-Za-z0-9_]*$')

# Valid timeframes
VALID_TIMEFRAMES = ["1m", "3m", "5m", "15m", "30m", "1h", "4h", "1d"]

# Valid template types
VALID_TEMPLATE_TYPES = ["basic", "scalper", "trend", "mean_reversion"]

STRATEGY_PARAMETER_NAMES = [
    "minimal_roi",
    "timeframe",
    "stoploss",
    "max_open_trades",
    "trailing_stop",
    "trailing_stop_positive",
    "trailing_stop_positive_offset",
    "trailing_only_offset_is_reached",
    "use_custom_stoploss",
    "process_only_new_candles",
    "order_types",
    "order_time_in_force",
    "unfilledtimeout",
    "disable_dataframe_checks",
    "use_exit_signal",
    "exit_profit_only",
    "exit_profit_offset",
    "ignore_roi_if_entry_signal",
    "ignore_buying_expired_candle_after",
    "position_adjustment_enable",
    "max_entry_position_adjustment",
]


class StrategyValidationError(Exception):
    """Raised when strategy validation fails"""
    pass


class StrategySecurityError(Exception):
    """Raised when a security violation is detected"""
    pass


def _extract_strategy_parameters(code: str, strategy_name: str) -> Dict[str, bool]:
    """Extract whether known strategy params are explicitly declared."""
    is_set: Dict[str, bool] = {k: False for k in STRATEGY_PARAMETER_NAMES}

    try:
        tree = ast.parse(code)
    except Exception:
        return is_set

    class_nodes = [n for n in tree.body if isinstance(n, ast.ClassDef)]
    if not class_nodes:
        return is_set

    target_class = next((c for c in class_nodes if c.name == strategy_name), class_nodes[0])

    for node in target_class.body:
        name = None
        value_node = None
        if isinstance(node, ast.Assign):
            if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
                name = node.targets[0].id
                value_node = node.value
        elif isinstance(node, ast.AnnAssign):
            if isinstance(node.target, ast.Name):
                name = node.target.id
                value_node = node.value

        if name in is_set:
            is_set[name] = True

    return is_set


def _upsert_strategy_component_metadata(
    db: Session,
    user_id: int,
    workspace_root: str,
    strategy_name: str,
    code: str,
    description: Optional[str] = None,
) -> None:
    """Upsert strategy metadata and parameter profile.

    This is best-effort and must not break strategy file operations.
    """
    target_path = _safe_resolve_strategy_path(workspace_root, strategy_name)
    is_set = _extract_strategy_parameters(code, strategy_name)

    component = (
        db.query(StrategyComponent)
        .filter(StrategyComponent.user_id == user_id, StrategyComponent.strategy_path == str(target_path))
        .first()
    )
    if not component:
        component = StrategyComponent(
            user_id=user_id,
            strategy_name=strategy_name,
            strategy_path=str(target_path),
        )
        db.add(component)
        db.flush()

    component.strategy_name = strategy_name

    params_row = db.query(StrategyParam).filter(StrategyParam.strategy_id == component.id).first()
    if not params_row:
        params_row = StrategyParam(strategy_id=component.id)
        db.add(params_row)

    for name in STRATEGY_PARAMETER_NAMES:
        setattr(params_row, name, bool(is_set.get(name, False)))

    if description is not None:
        component.description = description

    component.last_modified = datetime.now(timezone.utc)
    db.commit()


def validate_strategy_name(name: str) -> None:
    """
    Validate strategy name follows Python identifier rules.
    
    Args:
        name: Strategy name to validate
        
    Raises:
        StrategyValidationError: If name is invalid
    """
    if not name:
        raise StrategyValidationError("Strategy name cannot be empty")
    
    if len(name) > 100:
        raise StrategyValidationError("Strategy name too long (max 100 characters)")
    
    if not STRATEGY_NAME_PATTERN.match(name):
        raise StrategyValidationError(
            "Strategy name must start with a letter and contain only letters, numbers, and underscores"
        )
    
    if keyword.iskeyword(name):
        raise StrategyValidationError(f"'{name}' is a Python reserved keyword")


def _get_strategies_dir(workspace_root: str) -> Path:
    """Get the strategies directory for a workspace."""
    return Path(workspace_root) / "strategies"


def _make_readable(path: Path) -> None:
    """Make a strategy file readable by the Freqtrade container user."""
    try:
        os.chmod(path, 0o644)
    except Exception:
        pass


def _safe_resolve_strategy_path(workspace_root: str, strategy_name: str) -> Path:
    """
    Safely resolve strategy file path with security checks.
    
    Args:
        workspace_root: User's workspace root directory
        strategy_name: Name of the strategy (without .py extension)
        
    Returns:
        Path: Resolved and validated path
        
    Raises:
        StrategySecurityError: If path traversal detected
        StrategyValidationError: If name is invalid
    """
    validate_strategy_name(strategy_name)
    
    strategies_dir = _get_strategies_dir(workspace_root).resolve()
    target_path = (strategies_dir / f"{strategy_name}.py").resolve()
    
    # Security check: ensure target is within strategies directory
    try:
        target_path.relative_to(strategies_dir)
    except ValueError:
        raise StrategySecurityError(
            f"Path traversal detected: {strategy_name}"
        )
    
    return target_path


def list_strategies(workspace_root: str) -> List[Dict[str, any]]:
    """
    List all strategy files in user's strategies directory.
    
    Args:
        workspace_root: User's workspace root directory
        
    Returns:
        List of strategy info dicts with name, filename, modifiedAt, size
    """
    strategies_dir = _get_strategies_dir(workspace_root)
    
    if not strategies_dir.exists():
        return []
    
    strategies = []
    
    for py_file in strategies_dir.glob("*.py"):
        if py_file.name.startswith("_"):
            continue
        
        try:
            stat = py_file.stat()
            name = py_file.stem  # filename without .py
            
            strategies.append({
                "name": name,
                "filename": py_file.name,
                "modifiedAt": int(stat.st_mtime),
                "size": stat.st_size
            })
        except Exception:
            continue
    
    # Sort by name
    strategies.sort(key=lambda s: s["name"])
    
    return strategies


def read_strategy(workspace_root: str, strategy_name: str) -> Dict[str, str]:
    """
    Read strategy file content.
    
    Args:
        workspace_root: User's workspace root directory
        strategy_name: Name of the strategy (without .py extension)
        
    Returns:
        Dict with name, filename, and code
        
    Raises:
        FileNotFoundError: If strategy file doesn't exist
        StrategyValidationError: If file is too large
    """
    target_path = _safe_resolve_strategy_path(workspace_root, strategy_name)
    
    if not target_path.exists():
        raise FileNotFoundError(f"Strategy '{strategy_name}' not found")
    
    # Check file size
    if target_path.stat().st_size > MAX_STRATEGY_FILE_SIZE:
        raise StrategyValidationError(
            f"Strategy file too large (max {MAX_STRATEGY_FILE_SIZE // 1024}KB)"
        )
    
    # Read file
    try:
        code = target_path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        raise StrategyValidationError("Strategy file must be valid UTF-8")
    
    return {
        "name": strategy_name,
        "filename": f"{strategy_name}.py",
        "code": code
    }


def validate_strategy_code(code: str) -> tuple[bool, Optional[str]]:
    """
    Validate Python syntax without executing code.
    
    Args:
        code: Python code to validate
        
    Returns:
        Tuple of (is_valid, error_message)
    """
    if not code or not code.strip():
        return False, "Strategy code cannot be empty"
    
    if len(code) > MAX_STRATEGY_FILE_SIZE:
        return False, f"Strategy code too large (max {MAX_STRATEGY_FILE_SIZE // 1024}KB)"
    
    try:
        ast.parse(code)
        return True, None
    except SyntaxError as e:
        return False, f"Syntax error at line {e.lineno}: {e.msg}"
    except Exception as e:
        return False, f"Validation error: {str(e)}"


def update_strategy(
    workspace_root: str,
    strategy_name: str,
    code: str,
    db: Optional[Session] = None,
    user_id: Optional[int] = None,
    description: Optional[str] = None,
) -> Dict[str, str]:
    """
    Update strategy file content with validation.
    
    Args:
        workspace_root: User's workspace root directory
        strategy_name: Name of the strategy (without .py extension)
        code: New Python code
        
    Returns:
        Dict with success info
        
    Raises:
        FileNotFoundError: If strategy doesn't exist
        StrategyValidationError: If code is invalid
    """
    target_path = _safe_resolve_strategy_path(workspace_root, strategy_name)
    
    if not target_path.exists():
        raise FileNotFoundError(f"Strategy '{strategy_name}' not found")
    
    # Validate syntax
    is_valid, error = validate_strategy_code(code)
    if not is_valid:
        raise StrategyValidationError(error or "Invalid Python syntax")
    
    # Write to temp file first, then atomic replace
    strategies_dir = _get_strategies_dir(workspace_root)
    
    with tempfile.NamedTemporaryFile(
        mode='w',
        encoding='utf-8',
        dir=str(strategies_dir),
        delete=False,
        suffix='.tmp'
    ) as tmp_file:
        tmp_file.write(code)
        tmp_path = tmp_file.name
    
    try:
        # Atomic replace
        os.replace(tmp_path, str(target_path))
        _make_readable(target_path)
    except Exception as e:
        # Cleanup temp file on error
        try:
            os.unlink(tmp_path)
        except Exception:
            pass
        raise StrategyValidationError(f"Failed to save strategy: {str(e)}")

    if db is not None and user_id is not None:
        try:
            _upsert_strategy_component_metadata(
                db=db,
                user_id=int(user_id),
                workspace_root=workspace_root,
                strategy_name=strategy_name,
                code=code,
                description=description,
            )
        except Exception:
            # Strategy save must succeed even if metadata sync fails.
            pass
    
    return {
        "ok": True,
        "message": "Strategy saved successfully",
        "name": strategy_name
    }


def _generate_template(
    class_name: str,
    timeframe: str,
    can_short: bool,
    template_type: str
) -> str:
    """Generate strategy template based on type."""
    
    # Base template
    if template_type == "scalper":
        comments = """
    # Scalper strategy template
    # Focus on quick entries/exits with tight stops
    # Recommended: Use lower timeframes (1m, 3m, 5m)
"""
    elif template_type == "trend":
        comments = """
    # Trend following strategy template
    # Focus on riding established trends
    # Recommended: Use higher timeframes (15m, 1h, 4h)
"""
    elif template_type == "mean_reversion":
        comments = """
    # Mean reversion strategy template
    # Focus on price returning to average
    # Recommended: Use medium timeframes (5m, 15m, 30m)
"""
    else:  # basic
        comments = """
    # Basic strategy template
    # Customize as needed
"""
    
    short_methods = ""
    if can_short:
        short_methods = """
    def populate_entry_trend(self, df: DataFrame, metadata: dict | None = None):
        \"\"\"Define entry signals for long and short positions.\"\"\"
        df['enter_long'] = 0
        df['enter_short'] = 0
        
        # TODO: Add entry conditions
        # Example long entry:
        # long_mask = (df['close'] > df['ema_20']) & (df['rsi'] < 30)
        # df.loc[long_mask, 'enter_long'] = 1
        
        # Example short entry:
        # short_mask = (df['close'] < df['ema_20']) & (df['rsi'] > 70)
        # df.loc[short_mask, 'enter_short'] = 1
        
        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict | None = None):
        \"\"\"Define exit signals for long and short positions.\"\"\"
        df['exit_long'] = 0
        df['exit_short'] = 0
        
        # TODO: Add exit conditions
        
        return df
"""
    else:
        short_methods = """
    def populate_entry_trend(self, df: DataFrame, metadata: dict | None = None):
        \"\"\"Define entry signals for long positions.\"\"\"
        df['enter_long'] = 0
        
        # TODO: Add entry conditions
        # Example:
        # long_mask = (df['close'] > df['ema_20']) & (df['rsi'] < 30)
        # df.loc[long_mask, 'enter_long'] = 1
        
        return df

    def populate_exit_trend(self, df: DataFrame, metadata: dict | None = None):
        \"\"\"Define exit signals for long positions.\"\"\"
        df['exit_long'] = 0
        
        # TODO: Add exit conditions
        
        return df
"""
    
    template = f'''from __future__ import annotations

"""
Github_Or_Harari_OGAYN__strategy_file_service__20260723_122436 Strategy
{comments}
Generated strategy template - customize as needed.
"""

from pandas import DataFrame
# Uncomment if using CoreBaseStrategy:
# from backend.app.trading_core.base_strategy import CoreBaseStrategy

# For standard Freqtrade strategies, use:
from freqtrade.strategy import IStrategy


class Github_Or_Harari_OGAYN__strategy_file_service__20260723_122436(IStrategy):
    """
    Github_Or_Harari_OGAYN__strategy_file_service__20260723_122436 - Generated strategy template
    """
    
    # Strategy interface version
    INTERFACE_VERSION = 3
    
    # Strategy parameters
    timeframe = "{timeframe}"
    
    # Minimal ROI
    minimal_roi = {{
        "0": 0.10,    # 10% at any time
        "30": 0.05,   # 5% after 30 minutes
        "60": 0.02,   # 2% after 1 hour
    }}
    
    # Stoploss
    stoploss = -0.10
    
    # Trailing stop (optional)
    trailing_stop = False
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02
    trailing_only_offset_is_reached = True
    
    # Short trading (futures/margin)
    can_short = {str(can_short)}
    
    # Optional: Leverage settings (for futures)
    # leverage = {{
    #     "long": 1,
    #     "short": 1,
    # }}
    
    # Optional: Order types
    # order_types = {{
    #     "entry": "limit",
    #     "exit": "limit",
    #     "stoploss": "market",
    #     "stoploss_on_exchange": True
    # }}
    
    # Optional: Order time in force
    # order_time_in_force = {{
    #     "entry": "GTC",
    #     "exit": "GTC"
    # }}

    def populate_indicators(self, df: DataFrame, metadata: dict | None = None):
        \"\"\"
        Add indicators to the dataframe.
        
        Common indicators:
        - EMA/SMA: df['ema_20'] = ta.EMA(df['close'], timeperiod=20)
        - RSI: df['rsi'] = ta.RSI(df['close'], timeperiod=14)
        - MACD: df['macd'], df['macdsignal'], df['macdhist'] = ta.MACD(df['close'])
        - Bollinger Bands: df['bb_upper'], df['bb_middle'], df['bb_lower'] = ta.BBANDS(df['close'])
        \"\"\"
        
        # TODO: Add your indicators here
        # Example:
        # import talib.abstract as ta
        # df['ema_20'] = ta.EMA(df['close'], timeperiod=20)
        # df['rsi'] = ta.RSI(df['close'], timeperiod=14)
        
        return df
{short_methods}
    # Optional: Custom stoploss
    # def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
    #                     current_rate: float, current_profit: float, **kwargs) -> float:
    #     \"\"\"
    #     Custom stoploss logic.
    #     Return a negative value for stoploss, or None to keep current stoploss.
    #     \"\"\"
    #     return self.stoploss
    
    # Optional: Custom exit
    # def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
    #                 current_rate: float, current_profit: float, **kwargs) -> Optional[str]:
    #     \"\"\"
    #     Custom exit logic.
    #     Return string exit reason or None.
    #     \"\"\"
    #     return None
    
    # Optional: Confirm trade entry
    # def confirm_trade_entry(self, pair: str, order_type: str, amount: float,
    #                         rate: float, time_in_force: str, current_time: datetime,
    #                         entry_tag: Optional[str], side: str, **kwargs) -> bool:
    #     \"\"\"
    #     Confirm trade entry (last chance to cancel).
    #     Return True to proceed, False to cancel.
    #     \"\"\"
    #     return True
    
    # Optional: Confirm trade exit
    # def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str,
    #                        amount: float, rate: float, time_in_force: str,
    #                        exit_reason: str, current_time: datetime, **kwargs) -> bool:
    #     \"\"\"
    #     Confirm trade exit (last chance to cancel).
    #     Return True to proceed, False to cancel.
    #     \"\"\"
    #     return True
'''
    
    return template


def create_strategy(
    workspace_root: str,
    strategy_name: str,
    timeframe: str = "5m",
    can_short: bool = False,
    template_type: str = "basic",
    overwrite: bool = False,
    db: Optional[Session] = None,
    user_id: Optional[int] = None,
    description: Optional[str] = None,
) -> Dict[str, str]:
    """
    Create a new strategy file from template.
    
    Args:
        workspace_root: User's workspace root directory
        strategy_name: Name for the new strategy
        timeframe: Timeframe for the strategy
        can_short: Whether strategy can open short positions
        template_type: Type of template (basic, scalper, trend, mean_reversion)
        overwrite: Whether to overwrite existing file
        
    Returns:
        Dict with success info and path
        
    Raises:
        StrategyValidationError: If parameters are invalid
        FileExistsError: If file exists and overwrite=False
    """
    # Validate inputs
    validate_strategy_name(strategy_name)
    
    if timeframe not in VALID_TIMEFRAMES:
        raise StrategyValidationError(
            f"Invalid timeframe. Must be one of: {', '.join(VALID_TIMEFRAMES)}"
        )
    
    if template_type not in VALID_TEMPLATE_TYPES:
        raise StrategyValidationError(
            f"Invalid template type. Must be one of: {', '.join(VALID_TEMPLATE_TYPES)}"
        )
    
    # Resolve path
    target_path = _safe_resolve_strategy_path(workspace_root, strategy_name)
    
    # Check if exists
    if target_path.exists() and not overwrite:
        raise FileExistsError(f"Strategy '{strategy_name}' already exists")
    
    # Ensure strategies directory exists
    strategies_dir = _get_strategies_dir(workspace_root)
    strategies_dir.mkdir(parents=True, exist_ok=True)
    
    # Create __init__.py if needed
    init_path = strategies_dir / "__init__.py"
    if not init_path.exists():
        init_path.write_text("", encoding="utf-8")
        _make_readable(init_path)
    
    # Generate template
    code = _generate_template(
        class_name=strategy_name,
        timeframe=timeframe,
        can_short=can_short,
        template_type=template_type
    )
    
    # Validate generated code
    is_valid, error = validate_strategy_code(code)
    if not is_valid:
        raise StrategyValidationError(f"Generated template is invalid: {error}")
    
    # Write file
    target_path.write_text(code, encoding="utf-8")
    _make_readable(target_path)

    if db is not None and user_id is not None:
        try:
            _upsert_strategy_component_metadata(
                db=db,
                user_id=int(user_id),
                workspace_root=workspace_root,
                strategy_name=strategy_name,
                code=code,
                description=description,
            )
        except Exception:
            # Strategy creation must succeed even if metadata sync fails.
            pass
    
    return {
        "ok": True,
        "message": f"Strategy '{strategy_name}' created successfully",
        "name": strategy_name,
        "filename": f"{strategy_name}.py"
    }
