"""
키움 REST API 주문 호출 — 매수 kt10000 / 매도 kt10001 / 정정 kt10002 / 취소 kt10003.

* dry-run default. dry_run=False 명시 시에만 실주문.
* 진입점 첫 줄에서 sidecar.guard_or_raise() 호출 강제.
* 자동 재시도 금지 — 네트워크 에러 시 한 번만 시도하고 사람이 ord_no 조회로 재판단.
* 멱등성: 동일 (계좌·종목·side·수량·가격) 60초 윈도우 해시로 중복 차단.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Optional

_PARENT = Path(__file__).resolve().parent.parent
if str(_PARENT) not in sys.path:
    sys.path.insert(0, str(_PARENT))

import kiwoom_client as kc
from . import ledger, sidecar

LIMITS_FILE = Path(__file__).resolve().parent / 'limits.json'

TR_BUY = 'kt10000'
TR_SELL = 'kt10001'
TR_MODIFY = 'kt10002'
TR_CANCEL = 'kt10003'

# 매매구분 코드
TRDE_TP_LIMIT = '0'    # 보통가 (지정가)
TRDE_TP_MARKET = '3'   # 시장가
TRDE_TP_STOP = '28'    # 스톱지정가 — cond_uv(조건단가) 도달 시 ord_uv(지정가)로 발주

# 거래소 코드 (라우팅 suffix → API 거래소 구분)
_EXCHANGE_BY_SUFFIX = {
    '_AL': 'SOR',
    '_NX': 'NXT',
    '': 'KRX',
}


def _limits() -> dict:
    return json.loads(LIMITS_FILE.read_text(encoding='utf-8'))


def _exchange_for(suffix: str) -> str:
    if suffix not in _EXCHANGE_BY_SUFFIX:
        raise ValueError(f'unknown routing suffix: {suffix!r}')
    return _EXCHANGE_BY_SUFFIX[suffix]


def submit(account_label: str, side: str, symbol: str, qty: int,
           price: Optional[int], order_type: str, routing_suffix: str,
           dry_run: bool = True, card_id: Optional[str] = None,
           stop_price: Optional[int] = None) -> dict:
    sidecar.guard_or_raise()

    if side not in ('BUY', 'SELL'):
        raise ValueError(f'invalid side: {side}')
    if order_type not in ('LIMIT', 'MARKET', 'AGGRESSIVE_LIMIT', 'STOP_LIMIT'):
        raise ValueError(f'invalid order_type: {order_type}')
    if order_type in ('LIMIT', 'AGGRESSIVE_LIMIT') and not price:
        raise ValueError(f'{order_type} requires price')
    if order_type == 'STOP_LIMIT' and (not price or not stop_price):
        raise ValueError('STOP_LIMIT requires price(지정가) and stop_price(조건단가)')

    tr_id = TR_BUY if side == 'BUY' else TR_SELL
    exchange = _exchange_for(routing_suffix)
    is_market = order_type == 'MARKET'
    is_stop = order_type == 'STOP_LIMIT'

    if is_market:
        trde_tp = TRDE_TP_MARKET
    elif is_stop:
        trde_tp = TRDE_TP_STOP
    else:
        trde_tp = TRDE_TP_LIMIT

    body = {
        'dmst_stex_tp': exchange,
        'stk_cd': symbol,
        'ord_qty': str(qty),
        'ord_uv': '' if is_market else str(price),
        'trde_tp': trde_tp,
        'cond_uv': str(stop_price) if is_stop else '',
    }

    payload = {
        'card_id': card_id,
        'account': account_label,
        'side': side,
        'symbol': symbol,
        'qty': qty,
        'price': price,
        'stop_price': stop_price if is_stop else None,
        'order_type': order_type,
        'routing_suffix': routing_suffix,
        'exchange': exchange,
        'tr_id': tr_id,
        'dry_run': dry_run,
        'idem_hash': ledger.idempotency_hash(account_label, symbol, side, qty, price or 0),
    }

    if dry_run:
        ledger.append('dryrun', dict(payload, body=body))
        return {'ok': True, 'dry_run': True, 'payload': payload, 'body': body}

    if ledger.find_recent_idempotency(payload['idem_hash']):
        ledger.append('rejected', dict(payload, reason='IDEMPOTENCY_DUP'))
        return {'ok': False, 'reason': 'IDEMPOTENCY_DUP', 'payload': payload}

    try:
        url = kc.base_url() + '/api/dostk/ordr'
        resp = kc._http_post_json(url, body, kc.auth_headers(account_label, tr_id=tr_id))
        if resp.get('return_code', 0) != 0:
            msg = str(resp.get('return_msg') or '')
            if '8005' in msg or 'Token이 유효하지 않습니다' in msg:
                kc.issue_token(account_label, force=True)
                resp = kc._http_post_json(url, body, kc.auth_headers(account_label, tr_id=tr_id))
    except Exception as e:
        ledger.append('failed', dict(payload, error=repr(e)))
        return {'ok': False, 'reason': 'NETWORK', 'error': repr(e), 'payload': payload}

    # 키움 kt10000/kt10001 명세 응답 필드: ord_no, dmst_stex_tp, return_code, return_msg
    # (resp가 dict 아닌 경우는 위 try의 .get 호출에서 AttributeError로 NETWORK 분기됨)
    return_code = resp.get('return_code')
    if return_code != 0:
        ledger.append('rejected', dict(payload, response=resp))
        return {'ok': False, 'reason': 'BROKER_REJECT', 'response': resp, 'payload': payload}

    ord_no = resp.get('ord_no', '')
    ledger.append('submitted', dict(payload, ord_no=ord_no,
                                     response_summary={'return_code': return_code,
                                                       'return_msg': resp.get('return_msg', '')}))
    return {'ok': True, 'ord_no': ord_no, 'response': resp, 'payload': payload}


def _post_order_tr(tr_id: str, account_label: str, body: dict) -> dict:
    """주문 ordr endpoint POST + 토큰 만료 시 1회 재시도. 응답 raw dict 반환."""
    url = kc.base_url() + '/api/dostk/ordr'
    resp = kc._http_post_json(url, body, kc.auth_headers(account_label, tr_id=tr_id))
    if resp.get('return_code', 0) != 0:
        msg = str(resp.get('return_msg') or '')
        if '8005' in msg or 'Token이 유효하지 않습니다' in msg:
            kc.issue_token(account_label, force=True)
            resp = kc._http_post_json(url, body, kc.auth_headers(account_label, tr_id=tr_id))
    return resp


def cancel_order(account_label: str, orig_ord_no: str, symbol: str,
                  cancel_qty: int = 0, routing_suffix: str = '',
                  dry_run: bool = True, card_id: Optional[str] = None) -> dict:
    """미체결 주문 취소 (kt10003).

    cancel_qty=0 → 잔량 전부 취소 (키움 명세: '0' 입력시 잔량 전부 취소).
    routing_suffix 는 원주문의 거래소와 동일해야 함 — 호출자가 ka10075 응답의
    routing_suffix 그대로 전달하는 게 안전.
    """
    sidecar.guard_or_raise()

    if not orig_ord_no:
        raise ValueError('orig_ord_no required')
    if not symbol:
        raise ValueError('symbol required')
    if cancel_qty < 0:
        raise ValueError(f'cancel_qty must be >= 0 (got {cancel_qty})')

    exchange = _exchange_for(routing_suffix)
    body = {
        'dmst_stex_tp': exchange,
        'orig_ord_no': str(orig_ord_no),
        'stk_cd': symbol,
        'cncl_qty': str(cancel_qty),
    }

    payload = {
        'card_id': card_id,
        'account': account_label,
        'symbol': symbol,
        'orig_ord_no': orig_ord_no,
        'cancel_qty': cancel_qty,
        'routing_suffix': routing_suffix,
        'exchange': exchange,
        'tr_id': TR_CANCEL,
        'dry_run': dry_run,
    }

    if dry_run:
        ledger.append('dryrun', dict(payload, body=body, kind='cancel'))
        return {'ok': True, 'dry_run': True, 'payload': payload, 'body': body}

    try:
        resp = _post_order_tr(TR_CANCEL, account_label, body)
    except Exception as e:
        ledger.append('failed', dict(payload, error=repr(e), kind='cancel'))
        return {'ok': False, 'reason': 'NETWORK', 'error': repr(e), 'payload': payload}

    return_code = resp.get('return_code')
    if return_code != 0:
        ledger.append('cancel_rejected', dict(payload, response=resp))
        return {'ok': False, 'reason': 'BROKER_REJECT', 'response': resp, 'payload': payload}

    new_ord_no = resp.get('ord_no', '')
    ledger.append('cancel_submitted', dict(payload, new_ord_no=new_ord_no,
                                            response_summary={'return_code': return_code,
                                                              'return_msg': resp.get('return_msg', '')}))
    return {'ok': True, 'new_ord_no': new_ord_no, 'response': resp, 'payload': payload}


def modify_order(account_label: str, orig_ord_no: str, symbol: str,
                  modify_qty: int, modify_price: int,
                  routing_suffix: str = '',
                  dry_run: bool = True, card_id: Optional[str] = None,
                  modify_cond_price: Optional[int] = None) -> dict:
    """미체결 주문 정정 (kt10002).

    modify_qty / modify_price 둘 다 필수 — 키움 명세상 mdfy_qty/mdfy_uv 모두 Required.
    수량만 바꿀 땐 기존 가격, 가격만 바꿀 땐 기존 수량을 그대로 전달.
    시장가 주문은 정정 불가 (mdfy_uv 가 가격이라 0 불가) — 호출 전에 차단.
    routing_suffix 는 원주문의 거래소와 동일해야 함.

    modify_cond_price: 스톱지정가(trde_tp=28) 주문의 조건단가 정정용 (mdfy_cond_uv).
    트레일링 스톱이 고점 갱신 시 조건단가를 올리는 데 사용. 일반 주문은 None.
    ⚠️ 정정 응답의 ord_no 는 신규 주문번호 — 다음 정정은 그 번호를 orig_ord_no 로 써야 함.
    """
    sidecar.guard_or_raise()

    if not orig_ord_no:
        raise ValueError('orig_ord_no required')
    if not symbol:
        raise ValueError('symbol required')
    if modify_qty <= 0:
        raise ValueError(f'modify_qty must be > 0 (got {modify_qty})')
    if modify_price <= 0:
        raise ValueError(f'modify_price must be > 0 — 시장가 주문은 정정 불가, 취소 후 신규 발주')
    if modify_cond_price is not None and modify_cond_price <= 0:
        raise ValueError(f'modify_cond_price must be > 0 (got {modify_cond_price})')

    exchange = _exchange_for(routing_suffix)
    body = {
        'dmst_stex_tp': exchange,
        'orig_ord_no': str(orig_ord_no),
        'stk_cd': symbol,
        'mdfy_qty': str(modify_qty),
        'mdfy_uv': str(modify_price),
        'mdfy_cond_uv': str(modify_cond_price) if modify_cond_price else '',
    }

    payload = {
        'card_id': card_id,
        'account': account_label,
        'symbol': symbol,
        'orig_ord_no': orig_ord_no,
        'modify_qty': modify_qty,
        'modify_price': modify_price,
        'modify_cond_price': modify_cond_price,
        'routing_suffix': routing_suffix,
        'exchange': exchange,
        'tr_id': TR_MODIFY,
        'dry_run': dry_run,
    }

    if dry_run:
        ledger.append('dryrun', dict(payload, body=body, kind='modify'))
        return {'ok': True, 'dry_run': True, 'payload': payload, 'body': body}

    try:
        resp = _post_order_tr(TR_MODIFY, account_label, body)
    except Exception as e:
        ledger.append('failed', dict(payload, error=repr(e), kind='modify'))
        return {'ok': False, 'reason': 'NETWORK', 'error': repr(e), 'payload': payload}

    return_code = resp.get('return_code')
    if return_code != 0:
        ledger.append('modify_rejected', dict(payload, response=resp))
        return {'ok': False, 'reason': 'BROKER_REJECT', 'response': resp, 'payload': payload}

    new_ord_no = resp.get('ord_no', '')
    ledger.append('modify_submitted', dict(payload, new_ord_no=new_ord_no,
                                            response_summary={'return_code': return_code,
                                                              'return_msg': resp.get('return_msg', '')}))
    return {'ok': True, 'new_ord_no': new_ord_no, 'response': resp, 'payload': payload}
