import os
import requests
import jwt
import uuid
import hashlib
import time
import logging
from urllib.parse import urlencode, unquote
from dotenv import load_dotenv
from collections import deque
from datetime import datetime, timedelta

# .env 파일 로드 (스크립트 파일의 동일 디렉토리에 위치)
env_path = os.path.join(os.path.dirname(__file__), '.env')
load_dotenv(dotenv_path=env_path)

# Upbit API Keys 설정 (환경 변수에서 불러오기)
access_key = os.getenv('UPBIT_OPEN_API_ACCESS_KEY')
secret_key = os.getenv('UPBIT_OPEN_API_SECRET_KEY')
server_url = os.getenv('UPBIT_OPEN_API_SERVER_URL', 'https://api.upbit.com')

if not access_key:
    raise ValueError("Upbit access key not found. Please set 'UPBIT_OPEN_API_ACCESS_KEY' in your .env file.")
if not secret_key:
    raise ValueError("Upbit secret key not found. Please set 'UPBIT_OPEN_API_SECRET_KEY' in your .env file.")

# 로깅 설정
logger = logging.getLogger('trading_bot')
logger.setLevel(logging.DEBUG)

# 파일 핸들러 (DEBUG 이상)
file_handler = logging.FileHandler('trading_bot.log')
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)

# 콘솔 핸들러 (INFO 이상)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_formatter = logging.Formatter('%(asctime)s - %(message)s', datefmt='%H:%M:%S')
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)

# 틱 사이즈 계산 함수
def get_tick_size(price):
    """
    가격에 따른 틱 사이즈를 동적으로 지정합니다.
    UXLINK의 가격대에 맞게 조정 필요.
    예시:
        - 0.00001 KRW < price < 0.0001 KRW: tick_size = 0.00001
        - 0.0001 KRW <= price < 0.001 KRW: tick_size = 0.0001
        - ...
    """
    if price < 0.01:
        return 0.00001
    elif price < 0.1:
        return 0.0001
    elif price < 1:
        return 0.001
    elif price < 10:
        return 0.01
    elif price < 100:
        return 0.1
    else:
        return 1.0

# 수익성 검증 함수
def is_profit_possible(buy_price, sell_price, fee_rate=0.002):
    """
    수수료를 고려하여 매도 가격이 수익을 낼 수 있는지 확인합니다.
    fee_rate: 매수 + 매도 수수료 합산 (0.2% = 0.001 * 2)
    """
    total_fee_buy = buy_price * 0.001  # 매수 수수료 (0.1%)
    total_fee_sell = sell_price * 0.001  # 매도 수수료 (0.1%)
    total_cost = buy_price + total_fee_buy
    total_revenue = sell_price - total_fee_sell
    profit = total_revenue - total_cost
    logger.debug(f"Buy Price: {buy_price}, Sell Price: {sell_price}, Profit: {profit}")
    return profit > 0

# 주문 상태 추적을 위한 전역 변수 및 타임스탬프 큐
buy_order_timestamps = deque(maxlen=10)  # 최근 1분간의 매수 주문 타임스탬프 저장 (최대 10개)
sell_orders = {}  # {buy_order_id: sell_order_id}

# 매수/매도 주문 실행 함수
def place_order(market, side, volume, price, linked_order_id=None, ord_type='limit'):
    params = {
        'market': market,
        'side': side,
        'volume': str(volume),  # 문자열로 변환
        'price': str(price),    # 문자열로 변환
        'ord_type': ord_type,
    }
    
    if linked_order_id:
        params['linked_order_id'] = linked_order_id  # 매도 주문이 매수 주문과 연결되었음을 표시

    # URL 인코딩된 쿼리 문자열 생성 (unquote 사용)
    query_string = unquote(urlencode(params, doseq=True)).encode("utf-8")

    # SHA512 해시 생성
    m = hashlib.sha512()
    m.update(query_string)
    query_hash = m.hexdigest()

    # JWT 페이로드 구성
    payload = {
        'access_key': access_key,
        'nonce': str(uuid.uuid4()),
        'query_hash': query_hash,
        'query_hash_alg': 'SHA512',
    }

    # JWT 토큰 생성
    try:
        jwt_token = jwt.encode(payload, secret_key, algorithm='HS512')
        if isinstance(jwt_token, bytes):
            jwt_token = jwt_token.decode('utf-8')
    except Exception as e:
        logger.debug(f"Error encoding JWT: {e}")
        return None

    authorization = f'Bearer {jwt_token}'
    headers = {
        "Authorization": authorization
    }

    # POST 요청 전송 (json=params으로 전송)
    try:
        response = requests.post(f"{server_url}/v1/orders", json=params, headers=headers)
        response.raise_for_status()

        data = response.json()
        order_id = data.get('uuid')
        if order_id:
            if side == 'bid':
                buy_order_timestamps.append(datetime.now())  # 매수 주문 타임스탬프 추가
            elif side == 'ask' and linked_order_id:
                sell_orders[linked_order_id] = order_id
            return data
        return data
    except requests.exceptions.HTTPError as http_err:
        logger.debug(f"HTTP error occurred: {http_err} - Response: {response.text}")
    except Exception as e:
        logger.debug(f"Error placing order: {e}")
        logger.debug(f"Query string: {query_string}")
        logger.debug(f"Query hash: {query_hash}")

    return None

# 주문 상태 확인 함수
def check_order_status(order_id, side):
    """
    특정 주문의 상태를 확인하는 함수
    """
    url = f"{server_url}/v1/order"
    query = {
        'uuid': order_id
    }
    query_string = urlencode(query).encode("utf-8")

    m = hashlib.sha512()
    m.update(query_string)
    query_hash = m.hexdigest()

    payload = {
        'access_key': access_key,
        'nonce': str(uuid.uuid4()),
        'query_hash': query_hash,
        'query_hash_alg': 'SHA512',
    }

    try:
        jwt_token = jwt.encode(payload, secret_key, algorithm='HS512')
        if isinstance(jwt_token, bytes):
            jwt_token = jwt_token.decode('utf-8')
    except Exception as e:
        logger.debug(f"Error encoding JWT for order status: {e}")
        return None

    authorization = f'Bearer {jwt_token}'
    headers = {
        "Authorization": authorization
    }

    try:
        response = requests.get(url, params=query, headers=headers)
        response.raise_for_status()
        data = response.json()
        return data
    except requests.exceptions.HTTPError as http_err:
        logger.debug(f"HTTP error occurred while checking order status: {http_err} - Response: {response.text}")
    except Exception as e:
        logger.debug(f"Error checking order status: {e}")

    return None

# 주문 취소 함수
def cancel_order(order_id):
    """
    특정 주문을 취소하는 함수
    """
    url = f"{server_url}/v1/order"
    query = {
        'uuid': order_id
    }
    query_string = urlencode(query).encode("utf-8")

    m = hashlib.sha512()
    m.update(query_string)
    query_hash = m.hexdigest()

    payload = {
        'access_key': access_key,
        'nonce': str(uuid.uuid4()),
        'query_hash': query_hash,
        'query_hash_alg': 'SHA512',
    }

    try:
        jwt_token = jwt.encode(payload, secret_key, algorithm='HS512')
        if isinstance(jwt_token, bytes):
            jwt_token = jwt_token.decode('utf-8')
    except Exception as e:
        logger.debug(f"Error encoding JWT for canceling order: {e}")
        return None

    authorization = f'Bearer {jwt_token}'
    headers = {
        "Authorization": authorization
    }

    try:
        response = requests.delete(url, params=query, headers=headers)
        response.raise_for_status()
        data = response.json()
        return data
    except requests.exceptions.HTTPError as http_err:
        logger.debug(f"HTTP error occurred while canceling order: {http_err} - Response: {response.text}")
    except Exception as e:
        logger.debug(f"Error canceling order: {e}")
        logger.debug(f"Query string: {query_string}")
        logger.debug(f"Query hash: {query_hash}")

    return None

# 주문서 데이터 처리 함수
def process_orderbook(orderbook):
    try:
        orderbook_units = orderbook['orderbook_units']
        bids = []
        asks = []
        for unit in orderbook_units:
            bids.append({'price': float(unit['bid_price']), 'size': float(unit['bid_size'])})
            asks.append({'price': float(unit['ask_price']), 'size': float(unit['ask_size'])})
        return bids, asks
    except (KeyError, TypeError, IndexError, ValueError) as e:
        logger.debug(f"Error processing orderbook data: {e}")
        return None, None

# 매수 주문 실행 함수 (실시간)
def execute_buy_order_real_time():
    market = 'KRW-UXLINK'  # UXLINK 시장 심볼로 변경

    # Rate Limiting: 최근 1분간의 매수 주문 수 확인
    current_time = datetime.now()
    while buy_order_timestamps and current_time - buy_order_timestamps[0] > timedelta(minutes=1):
        buy_order_timestamps.popleft()

    if len(buy_order_timestamps) >= 10:
        logger.info("매수 주문 제한에 도달했습니다 (1분당 최대 10회). 잠시 대기합니다.")
        return

    orderbook = fetch_orderbook(market)
    if not orderbook:
        logger.debug("Failed to fetch orderbook.")
        return

    bids, asks = process_orderbook(orderbook)
    if not bids or not asks:
        logger.debug("Failed to process orderbook.")
        return

    best_bid = bids[0]['price']
    best_ask = asks[0]['price']
    mid_price = (best_bid + best_ask) / 2

    # 매수 가격 설정 (스프레드 최소 0.3원 또는 UXLINK의 가격대에 맞게 조정)
    tick_size = get_tick_size(mid_price)
    decimal_places = len(str(tick_size).split('.')[-1]) if '.' in str(tick_size) else 0
    buy_price = round(mid_price - 0.3, decimal_places)
    logger.debug(f"Calculated buy price: {buy_price} with tick size: {tick_size}")

    # 주문 수량 설정 (예: UXLINK의 가격대에 맞게 조정)
    volume = '15'  # UXLINK 가격에 따라 적절히 조정 (예: 1000 UXLINK)

    # 주문 실행
    response_buy = place_order(market, 'bid', volume, buy_price)

    current_time_str = time.strftime("%H:%M:%S", time.localtime())

    if response_buy and 'uuid' in response_buy:
        logger.info(f"{current_time_str} - 매수 주문: {buy_price}원, 수량: {volume} UXLINK")
    else:
        logger.info(f"{current_time_str} - 매수 주문 실행 실패")
        if response_buy and 'uuid' not in response_buy:
            logger.info(f"{current_time_str} - 매수 주문 실패")

# 매수 주문 체결 시 즉시 매도 주문을 실행하는 함수
def place_sell_order_on_buy(order_id, buy_price, volume):
    fee_rate = 0.001  # 0.1% 매수
    target_profit_rate = 0.002  # 0.2% 목표 수익률

    # 수수료를 고려한 매도 가격 설정
    sell_price = buy_price * (1 + target_profit_rate + fee_rate)
    tick_size = get_tick_size(sell_price)
    # 소수점 자리수를 맞추기 위하여 정수형으로 처리
    rounded_sell_price = round(sell_price / tick_size) * tick_size

    logger.debug(f"Setting sell price to {rounded_sell_price} based on buy price {buy_price} with tick size {tick_size}")

    # 수익성 검증
    if not is_profit_possible(buy_price, rounded_sell_price):
        logger.debug(f"Immediate sell profit not possible: Buy at {buy_price}, Sell at {rounded_sell_price}")
        return

    response_sell = place_order('KRW-UXLINK', 'ask', volume, rounded_sell_price, linked_order_id=order_id)

    current_time_str = time.strftime("%H:%M:%S", time.localtime())

    if response_sell and 'uuid' in response_sell:
        sell_order_id = response_sell['uuid']
        logger.info(f"{current_time_str} - 매도 주문 실행: {rounded_sell_price}원, 수량: {volume} UXLINK (매수 주문 ID: {order_id})")
    else:
        logger.info(f"{current_time_str} - 매도 주문 실행 실패: {rounded_sell_price}원, 수량: {volume} UXLINK (매수 주문 ID: {order_id})")

# 매도 주문 상태 확인 및 관리 함수
def monitor_sell_orders():
    global sell_orders
    while True:
        time.sleep(5)  # 5초마다 매도 주문 상태 확인
        with open_order_lock():
            for buy_id, sell_id in list(sell_orders.items()):
                status = check_order_status(sell_id, 'ask')
                if status:
                    state = status.get('state')
                    current_time_str = time.strftime("%H:%M:%S", time.localtime())
                    if state == 'done':
                        logger.info(f"{current_time_str} - 매도 주문 완료: {status.get('price')}원, 주문 ID: {sell_id}")
                        del sell_orders[buy_id]
                    elif state in ['cancelled', 'failed']:
                        logger.info(f"{current_time_str} - 매도 주문 취소됨: {status.get('price')}원, 주문 ID: {sell_id}")
                        del sell_orders[buy_id]

# 주문 상태 모니터링 및 관리 함수
def monitor_buy_orders():
    # 현재 코드에서는 매수 주문이 체결되었을 때만 매도 주문을 설정
    pass  # 매수 주문 모니터링은 execute_buy_order_real_time 함수에서 처리됨

# 실시간 시장 데이터 및 매수 주문 스케줄러 함수
def real_time_order_scheduler():
    while True:
        execute_buy_order_real_time()
        # 매수 주문 사이에 짧은 대기 시간 (예: 1초) 설정하여 API 호출 빈도를 조절
        time.sleep(1)

# 실시간 주문서 가져오기 함수
def fetch_orderbook(market):
    url = f"{server_url}/v1/orderbook"
    params = {'markets': market}
    try:
        response = requests.get(url, params=params)
        response.raise_for_status()
        orderbook_data = response.json()
        return orderbook_data[0] if orderbook_data else None
    except Exception as e:
        logger.debug(f"Error fetching orderbook: {e}")
        return None

# 스레드 간 안전한 파일 핸들러 (잠금 메커니즘)
from contextlib import contextmanager
import threading

file_lock = threading.Lock()

@contextmanager
def open_order_lock():
    file_lock.acquire()
    try:
        yield
    finally:
        file_lock.release()

if __name__ == "__main__":
    # 매도 주문 상태 모니터링 스레드 시작
    monitor_sell_thread = threading.Thread(target=monitor_sell_orders)
    monitor_sell_thread.daemon = True
    monitor_sell_thread.start()
    logger.debug("Sell order monitoring thread started.")

    # 실시간 주문 스케줄러 스레드 시작
    real_time_order_thread = threading.Thread(target=real_time_order_scheduler)
    real_time_order_thread.daemon = True
    real_time_order_thread.start()
    logger.debug("Real-time order scheduler thread started.")

    logger.info("Trading bot is running with real-time grid strategy for UXLINK. Press Ctrl+C to exit.")

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        logger.info("Trading bot stopped by user")


이 코드에서 제발 지정가 매도가 작동하게 해줘.. 
1분에 10번 주문 이거도 말이 이상한데, 그냥 무지성 매수 10회가 아니라, 어 이거 매수하고 매도하면 이익이 나겠는데? 싶은 구간이 정해지"면" 그 때 매수 -> 지정가 매도 이렇게 진행되게 해줘.. 다만 이 갭은 5초로 정해줘. 아무리 맛있는 계산이 나와도 최소 5초는 주문 과 주문 사이에 대기