import asyncio
import json
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Tuple

BASE = Path('/home/arin/.openclaw/workspace/projects/auto-trader')
CONFIG_PATH = BASE / 'strategy_config.json'
STATE_PATH = BASE / 'state.json'
SIGNALS_PATH = BASE / 'signals.jsonl'
ORDERS_PATH = BASE / 'orders.jsonl'
ALERTS_PATH = BASE / 'alerts.jsonl'

import sys
sys.path.insert(0, '/home/arin/.openclaw/workspace/KIS_MCP_Server')
from server import inquery_stock_price, inquery_balance, order_stock  # type: ignore

LAST_API_CALL_AT = 0.0


def load_json(path: Path, default: Any):
    if not path.exists():
        return default
    try:
        return json.loads(path.read_text(encoding='utf-8'))
    except Exception:
        return default


def save_json(path: Path, data: Any) -> None:
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')


def append_jsonl(path: Path, row: Dict[str, Any]) -> None:
    with open(path, 'a', encoding='utf-8') as f:
        f.write(json.dumps(row, ensure_ascii=False) + '\n')


def to_int(v: Any) -> int:
    try:
        return int(str(v).replace(',', '').strip())
    except Exception:
        return 0


def to_float(v: Any) -> float:
    try:
        return float(str(v).replace(',', '').strip())
    except Exception:
        return 0.0


def now_ts() -> str:
    return datetime.now().strftime('%Y-%m-%d %H:%M:%S')


def get_api_interval_seconds(cfg: Dict[str, Any]) -> float:
    return max(0.0, float(cfg.get('api_call_interval_ms', 300)) / 1000.0)


def get_retry_count(cfg: Dict[str, Any]) -> int:
    return max(0, int(cfg.get('api_retry_count', 2)))


def get_retry_backoff_seconds(cfg: Dict[str, Any]) -> float:
    return max(0.0, float(cfg.get('api_retry_backoff_ms', 600)) / 1000.0)


async def throttle_api_call(cfg: Dict[str, Any]) -> None:
    global LAST_API_CALL_AT
    interval = get_api_interval_seconds(cfg)
    now = time.monotonic()
    wait_for = interval - (now - LAST_API_CALL_AT)
    if wait_for > 0:
        await asyncio.sleep(wait_for)
    LAST_API_CALL_AT = time.monotonic()


async def call_with_retry(func, *args, cfg: Dict[str, Any], **kwargs):
    retries = get_retry_count(cfg)
    backoff = get_retry_backoff_seconds(cfg)
    last_error = None
    for attempt in range(retries + 1):
        try:
            await throttle_api_call(cfg)
            return await func(*args, **kwargs)
        except Exception as e:
            last_error = e
            if attempt >= retries:
                break
            await asyncio.sleep(backoff * (attempt + 1))
    raise last_error


def market_is_open(now: datetime | None = None) -> bool:
    now = now or datetime.now()
    if now.weekday() >= 5:
        return False
    hhmm = now.hour * 100 + now.minute
    return 900 <= hhmm <= 1530


async def fetch_balance_map(cfg: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
    raw = await call_with_retry(inquery_balance, cfg=cfg)
    items = raw.get('output1', []) if isinstance(raw, dict) else []
    result = {}
    for item in items:
        qty = to_int(item.get('hldg_qty', 0))
        if qty <= 0:
            continue
        symbol = item.get('pdno')
        if not symbol:
            continue
        result[symbol] = {
            'symbol': symbol,
            'name': item.get('prdt_name', ''),
            'qty': qty,
            'avg_price': to_float(item.get('pchs_avg_pric', 0)),
            'profit_rate': to_float(item.get('evlu_pfls_rt', 0)),
            'profit_amount': to_int(item.get('evlu_pfls_amt', 0)),
            'position_value': to_int(item.get('evlu_amt', 0)),
        }
    return result


async def fetch_quote(symbol: str, cfg: Dict[str, Any]) -> Dict[str, Any]:
    q = await call_with_retry(inquery_stock_price, symbol, cfg=cfg)
    return {
        'symbol': symbol,
        'name': q.get('hts_kor_isnm', ''),
        'price': to_int(q.get('stck_prpr', 0)),
        'open': to_int(q.get('stck_oprc', 0)),
        'high': to_int(q.get('stck_mxpr', 0)),
        'low': to_int(q.get('stck_llam', 0)),
        'prev_close': to_int(q.get('stck_prdy_clpr', 0)),
        'change_pct': to_float(q.get('prdy_ctrt', 0)),
        'volume': to_int(q.get('acml_vol', 0)),
        'trading_value': to_int(q.get('acml_tr_pbmn', 0)),
    }


def pct(a: float, b: float) -> float:
    if not b:
        return 0.0
    return (a - b) / b * 100.0


def eval_buy_rules(quote: Dict[str, Any], holding: Dict[str, Any] | None, cfg: Dict[str, Any], state: Dict[str, Any]) -> List[str]:
    rules = cfg['rules']
    signals: List[str] = []
    price = quote['price']
    open_ = quote['open']
    high = quote['high']
    low = quote['low']
    prev_close = quote['prev_close']

    if holding:
        return signals

    r1 = rules['buy_gap_strength']
    if r1['enabled']:
        if pct(price, prev_close) >= r1['min_pct_vs_prev_close'] and (not r1['require_above_open'] or price > open_):
            signals.append('buy_gap_strength')

    r2 = rules['buy_reclaim_after_dip']
    if r2['enabled']:
        dipped = pct(low, open_) <= -abs(r2['dip_below_open_pct'])
        reclaimed = price > open_ if r2['reclaim_above_open'] else True
        rebound = pct(price, low) >= r2['rebound_from_low_pct']
        if dipped and reclaimed and rebound:
            signals.append('buy_reclaim_after_dip')

    r3 = rules['buy_near_day_high']
    if r3['enabled'] and high > 0:
        distance_from_high = (high - price) / high * 100.0
        positive = quote['change_pct'] > 0 if r3['require_positive_day'] else True
        if distance_from_high <= r3['max_distance_from_high_pct'] and positive:
            signals.append('buy_near_day_high')

    symbol_state = state.setdefault(quote['symbol'], {'buy_count_today': 0, 'last_buy_date': None})
    today = datetime.now().strftime('%Y-%m-%d')
    if symbol_state.get('last_buy_date') != today:
        symbol_state['buy_count_today'] = 0
    if symbol_state.get('buy_count_today', 0) >= cfg['max_daily_buys_per_symbol']:
        return []

    return signals


def eval_sell_rules(quote: Dict[str, Any], holding: Dict[str, Any] | None, cfg: Dict[str, Any]) -> List[str]:
    if not holding:
        return []
    rules = cfg['rules']
    signals: List[str] = []

    profit_rate = holding.get('profit_rate', 0.0)
    price = quote['price']
    open_ = quote['open']

    r4 = rules['sell_take_profit']
    if r4['enabled'] and profit_rate >= r4['take_profit_pct']:
        signals.append('sell_take_profit')

    r5 = rules['sell_stop_loss_or_fade']
    fade_pct = pct(price, open_)
    if r5['enabled'] and (profit_rate <= r5['stop_loss_pct'] or fade_pct <= r5['fade_from_open_pct']):
        signals.append('sell_stop_loss_or_fade')

    return signals


def calc_buy_qty(price: int, cfg: Dict[str, Any], holding: Dict[str, Any] | None) -> int:
    if price <= 0:
        return 0
    budget = int(cfg['buy_budget_per_trade'])
    max_pos = int(cfg['max_position_value_per_symbol'])
    current_value = int(holding['position_value']) if holding else 0
    room = max(0, max_pos - current_value)
    usable = min(budget, room)
    qty = usable // price
    return max(0, qty)


def make_alert_key(symbol: str, side: str, reasons: List[str], ts: str) -> str:
    minute = ts[:16]
    return f"{minute}|{symbol}|{side}|{'/'.join(sorted(reasons))}"


def maybe_record_alert(symbol: str, name: str, side: str, reasons: List[str], quote: Dict[str, Any], state: Dict[str, Any]) -> Dict[str, Any] | None:
    if not reasons:
        return None
    ts = now_ts()
    key = make_alert_key(symbol, side, reasons, ts)
    alert_state = state.setdefault('_alerts', {})
    if alert_state.get('last_key') == key:
        return None
    alert = {
        'time': ts,
        'key': key,
        'symbol': symbol,
        'name': name,
        'side': side,
        'reasons': reasons,
        'price': quote.get('price', 0),
        'change_pct': quote.get('change_pct', 0),
    }
    append_jsonl(ALERTS_PATH, alert)
    alert_state['last_key'] = key
    return alert


async def execute_buy(symbol: str, quote: Dict[str, Any], reasons: List[str], cfg: Dict[str, Any], state: Dict[str, Any]) -> Dict[str, Any]:
    qty = calc_buy_qty(quote['price'], cfg, None)
    if qty <= 0:
        return {'action': 'buy_skipped', 'symbol': symbol, 'reason': 'budget_or_position_limit', 'time': now_ts()}

    row = {
        'time': now_ts(),
        'action': 'buy',
        'symbol': symbol,
        'name': quote['name'],
        'qty': qty,
        'price': quote['price'],
        'reasons': reasons,
        'dry_run': not cfg['enable_orders'],
    }
    if cfg['enable_orders']:
        row['result'] = await call_with_retry(order_stock, symbol, qty, 0, 'buy', cfg=cfg)
    append_jsonl(ORDERS_PATH, row)
    symbol_state = state.setdefault(symbol, {'buy_count_today': 0, 'last_buy_date': None})
    symbol_state['buy_count_today'] = int(symbol_state.get('buy_count_today', 0)) + 1
    symbol_state['last_buy_date'] = datetime.now().strftime('%Y-%m-%d')
    return row


async def execute_sell(symbol: str, quote: Dict[str, Any], holding: Dict[str, Any], reasons: List[str], cfg: Dict[str, Any]) -> Dict[str, Any]:
    qty = int(holding['qty']) if cfg.get('sell_all_on_signal', True) else 1
    row = {
        'time': now_ts(),
        'action': 'sell',
        'symbol': symbol,
        'name': quote['name'],
        'qty': qty,
        'price': quote['price'],
        'reasons': reasons,
        'dry_run': not cfg['enable_orders'],
    }
    if cfg['enable_orders']:
        row['result'] = await call_with_retry(order_stock, symbol, qty, 0, 'sell', cfg=cfg)
    append_jsonl(ORDERS_PATH, row)
    return row


async def run_once() -> Dict[str, Any]:
    cfg = load_json(CONFIG_PATH, {})
    state = load_json(STATE_PATH, {})
    balance_map = await fetch_balance_map(cfg)

    results = []
    for item in cfg.get('symbols', []):
        if not item.get('enabled', True):
            continue
        symbol = item['symbol']
        holding = balance_map.get(symbol)

        try:
            quote = await fetch_quote(symbol, cfg)
        except Exception as e:
            error_row = {
                'time': now_ts(),
                'symbol': symbol,
                'name': item.get('name', ''),
                'error': str(e),
            }
            append_jsonl(SIGNALS_PATH, error_row)
            results.append(error_row)
            continue

        buy_reasons = eval_buy_rules(quote, holding, cfg, state)
        sell_reasons = eval_sell_rules(quote, holding, cfg)

        signal_row = {
            'time': now_ts(),
            'symbol': symbol,
            'name': quote['name'],
            'quote': quote,
            'holding': holding,
            'buy_reasons': buy_reasons,
            'sell_reasons': sell_reasons,
        }
        append_jsonl(SIGNALS_PATH, signal_row)

        action = None
        if market_is_open():
            if sell_reasons and holding:
                maybe_record_alert(symbol, quote['name'], 'sell', sell_reasons, quote, state)
                action = await execute_sell(symbol, quote, holding, sell_reasons, cfg)
            elif buy_reasons and not holding:
                maybe_record_alert(symbol, quote['name'], 'buy', buy_reasons, quote, state)
                action = await execute_buy(symbol, quote, buy_reasons, cfg, state)
        else:
            action = {'action': 'market_closed', 'symbol': symbol, 'time': now_ts()}

        results.append({
            'symbol': symbol,
            'name': quote['name'],
            'price': quote['price'],
            'buy_reasons': buy_reasons,
            'sell_reasons': sell_reasons,
            'action': action,
        })

    save_json(STATE_PATH, state)
    return {'time': now_ts(), 'mode': cfg.get('mode'), 'enable_orders': cfg.get('enable_orders'), 'results': results}


async def main():
    cfg = load_json(CONFIG_PATH, {})
    poll_seconds = int(cfg.get('poll_seconds', 60))
    while True:
        try:
            result = await run_once()
            print(json.dumps(result, ensure_ascii=False))
        except Exception as e:
            print(json.dumps({'time': now_ts(), 'error': str(e)}, ensure_ascii=False))
        await asyncio.sleep(poll_seconds)


if __name__ == '__main__':
    asyncio.run(main())
