"""주문 접수 후 체결·취소 추적 — on-demand 데몬 패턴.

알바 측:
  watch(...) [fill kind] / watch_cancel(...) [cancel kind]
  → 큐 파일(state/fill_pending.jsonl)에 entry append + 데몬 ensure_running.

데몬 측 (orders/fill_watcher_daemon.py main):
  큐 파일 읽어 _FillWatcher._tracked 에 동기화
  → kt00007 폴링(체결 추적) + ka10075 폴링(취소 추적, cancel watch 존재 시만)
  → 알림 → 추적 끝난 entry 큐에서 제거 → 큐 비면 자기 종료(sys.exit + PID 파일 삭제).

폴링 스케줄 (모든 주문 공통, 가장 어린 주문 경과 시간 기준):
- 0~30초: 5초 간격
- 30~120초: 10초 간격
- 120~600초: 30초 간격
- 600~1800초: 60초 간격
- 1800초(30분) 경과 시 미체결/미확인 알림 1회 + 추적 종료

cancel kind: 원주문(orig_ord_no)이 ka10075 미체결 목록에서 사라지면 취소 확정.
사용자가 직접 발주한 fill watch 가 동시에 있으면 mdfy_cncl 발생 시 '사후거절' 메시지를
억제(취소 watch 가 확정 메시지 책임).
"""

from __future__ import annotations

import contextlib
import fcntl
import json
import os
import subprocess
import sys
import threading
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Callable, Optional

from . import card, ledger

UNFILLED_TIMEOUT_SECONDS = 1800  # 30분


def _spawn_journal_collect() -> None:
    """전량 체결(filled) 직후 trade_journal.collect 비동기 호출.
    scripts/는 _SCRIPTS_DIR(=parent) 기준 sys.path 추가 후 import.
    실패해도 fill 흐름·21:00 launchd 재적재에 영향 없음."""
    def _run():
        try:
            if str(_SCRIPTS_DIR) not in sys.path:
                sys.path.insert(0, str(_SCRIPTS_DIR))
            import trade_journal as tj
            tj.collect(quiet=True)
        except Exception as e:
            sys.stderr.write(f'[fill→journal] collect failed: {e}\n')
    threading.Thread(target=_run, daemon=True).start()

_ORDERS_DIR = Path(__file__).resolve().parent
_SCRIPTS_DIR = _ORDERS_DIR.parent
_WORKSPACE_ROOT = _SCRIPTS_DIR.parent
_STATE_DIR = _WORKSPACE_ROOT / 'state'
QUEUE_FILE = _STATE_DIR / 'fill_pending.jsonl'
QUEUE_LOCK = _STATE_DIR / 'fill_pending.jsonl.lock'
PID_FILE = _STATE_DIR / 'fill_watcher.pid'
# 체결 발생 신호 파일 — behive_web(별도 프로세스)이 /api/realtime/quotes 응답에 값을 실어주고,
# 브라우저가 값 변화 감지 시 자산웹 패널을 자동 새로고침(수량·예수금·당일정산 반영). file 기반 IPC.
FILL_SIGNAL_FILE = _STATE_DIR / 'fill_signal'


def _bump_fill_signal() -> None:
    """체결(전량/부분) 시 신호파일 갱신 → behive_web 패널 자동 새로고침 트리거. 실패 무시."""
    try:
        FILL_SIGNAL_FILE.write_text(str(time.time()), encoding='utf-8')
    except Exception:
        pass


# ---------- Tracked entry ----------

@dataclass
class Tracked:
    ord_no: str
    account: str
    side: str
    symbol: str
    symbol_name: str
    order_qty: int
    price: Optional[int]
    order_type: str
    card_id: str
    started_at: float
    last_cntr_qty: int = 0
    kind: str = 'fill'  # 'fill' (체결추적) | 'cancel' (취소확정추적)
    orig_ord_no: Optional[str] = None  # cancel kind 전용 — 취소 대상 원주문 번호


# ---------- 큐 파일 IO (atomic, file-locked) ----------

@contextlib.contextmanager
def _file_lock(path: Path):
    path.parent.mkdir(parents=True, exist_ok=True)
    fp = open(path, 'w')
    try:
        fcntl.flock(fp, fcntl.LOCK_EX)
        yield
    finally:
        try:
            fcntl.flock(fp, fcntl.LOCK_UN)
        finally:
            fp.close()


def append_queue_entry(entry: dict) -> None:
    """큐 파일에 한 줄 append (lock 보호)."""
    with _file_lock(QUEUE_LOCK):
        QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True)
        with QUEUE_FILE.open('a', encoding='utf-8') as f:
            f.write(json.dumps(entry, ensure_ascii=False) + '\n')


def read_queue() -> list[dict]:
    """큐 파일을 읽어 entry 리스트 반환. 빈 줄/잘못된 JSON 은 스킵."""
    if not QUEUE_FILE.exists():
        return []
    with _file_lock(QUEUE_LOCK):
        out: list[dict] = []
        for line in QUEUE_FILE.read_text(encoding='utf-8').splitlines():
            line = line.strip()
            if not line:
                continue
            try:
                out.append(json.loads(line))
            except ValueError:
                continue
        return out


def persist_queue(entries: list[dict]) -> None:
    """큐 파일 전체 다시 쓰기 (rewrite)."""
    with _file_lock(QUEUE_LOCK):
        QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True)
        if not entries:
            if QUEUE_FILE.exists():
                try:
                    QUEUE_FILE.unlink()
                except OSError:
                    QUEUE_FILE.write_text('', encoding='utf-8')
            return
        body = '\n'.join(json.dumps(e, ensure_ascii=False) for e in entries) + '\n'
        tmp = QUEUE_FILE.with_suffix(QUEUE_FILE.suffix + '.tmp')
        tmp.write_text(body, encoding='utf-8')
        os.replace(tmp, QUEUE_FILE)


# ---------- PID 파일 / 데몬 기동 ----------

def is_daemon_alive() -> bool:
    """PID 파일 기반 데몬 생존 확인. stale 자동 검출."""
    if not PID_FILE.exists():
        return False
    try:
        pid = int(PID_FILE.read_text().strip())
    except (ValueError, OSError):
        return False
    if pid <= 0:
        return False
    try:
        os.kill(pid, 0)
        return True
    except ProcessLookupError:
        return False
    except PermissionError:
        # 다른 사용자 PID 와 충돌 — 매우 드묾. 살아있다고 보수적 가정.
        return True


def ensure_daemon_running() -> None:
    """데몬 살아있으면 패스, 죽었으면 fork. 두 알바 동시 호출에도 PID 파일 lock 으로 한 데몬만 살아남음."""
    if is_daemon_alive():
        return
    # 데몬 자체가 PID 파일 작성·정리 — 알바는 fork 만.
    subprocess.Popen(
        [sys.executable, '-m', 'orders.fill_watcher_daemon'],
        cwd=str(_SCRIPTS_DIR),
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        stdin=subprocess.DEVNULL,
        start_new_session=True,
    )


# ---------- 데몬 entry — _FillWatcher 가 사용 ----------

class _FillWatcher:
    """데몬 프로세스 안에서 사용되는 추적 워커. in-memory _tracked + 텔레그램·키움 콜백."""

    def __init__(self):
        self._lock = threading.RLock()
        self._tracked: dict[str, Tracked] = {}
        self._send: Callable[[str], bool] = lambda msg: True
        self._fetch_executions: Callable[[str], list[dict]] = lambda account: []
        self._fetch_open_orders: Callable[[str], list[dict]] = lambda account: []

    def configure(self, send_func, fetch_executions, fetch_open_orders=None) -> None:
        with self._lock:
            self._send = send_func
            self._fetch_executions = fetch_executions
            if fetch_open_orders is not None:
                self._fetch_open_orders = fetch_open_orders

    def sync_from_queue(self, entries: list[dict]) -> None:
        """큐 파일 entry 리스트로 _tracked 동기화. 큐에 있고 _tracked 에 없으면 추가, 큐에서 사라진 ord_no 는 _tracked 에서도 제거."""
        with self._lock:
            queue_ord_nos = {e['ord_no'] for e in entries}
            # 추가
            for e in entries:
                ord_no = e['ord_no']
                if ord_no not in self._tracked:
                    self._tracked[ord_no] = Tracked(
                        ord_no=ord_no, account=e['account'], side=e['side'],
                        symbol=e['symbol'], symbol_name=e['symbol_name'],
                        order_qty=int(e['order_qty']),
                        price=e.get('price'),
                        order_type=e['order_type'], card_id=e['card_id'],
                        started_at=float(e['started_at']),
                        last_cntr_qty=int(e.get('last_cntr_qty', 0)),
                        kind=e.get('kind', 'fill'),
                        orig_ord_no=e.get('orig_ord_no'),
                    )
                else:
                    # 이미 있으면 last_cntr_qty 만 동기화 (큐가 진실 소스)
                    self._tracked[ord_no].last_cntr_qty = int(e.get('last_cntr_qty', 0))
            # 제거
            for ord_no in list(self._tracked.keys()):
                if ord_no not in queue_ord_nos:
                    self._tracked.pop(ord_no, None)

    def snapshot_entries(self) -> list[dict]:
        """현재 _tracked 를 큐 파일에 쓸 수 있는 entry 리스트로 직렬화."""
        with self._lock:
            return [asdict(t) for t in self._tracked.values()]

    def _next_sleep_seconds(self) -> int:
        with self._lock:
            if not self._tracked:
                return 5
            now = time.time()
            min_elapsed = min(now - t.started_at for t in self._tracked.values())
        if min_elapsed < 30:
            return 5
        if min_elapsed < 120:
            return 10
        if min_elapsed < 600:
            return 30
        return 60

    def _poll_once(self) -> None:
        with self._lock:
            fill_accounts = sorted({t.account for t in self._tracked.values()
                                     if t.kind == 'fill'})
            cancel_accounts = sorted({t.account for t in self._tracked.values()
                                       if t.kind == 'cancel'})
            tracked_snapshot = dict(self._tracked)

        rows_by_account: dict[str, list[dict]] = {}
        for account in fill_accounts:
            try:
                rows_by_account[account] = self._fetch_executions(account) or []
            except Exception as e:
                ledger.append('rejected', {'reason': 'FILL_WATCHER_FETCH_ERROR',
                                            'account': account, 'message': repr(e)})

        open_orders_by_account: dict[str, list[dict]] = {}
        for account in cancel_accounts:
            try:
                open_orders_by_account[account] = self._fetch_open_orders(account) or []
            except Exception as e:
                ledger.append('rejected', {'reason': 'CANCEL_WATCHER_FETCH_ERROR',
                                            'account': account, 'message': repr(e)})

        now = time.time()
        for ord_no, t in tracked_snapshot.items():
            elapsed = now - t.started_at
            if t.kind == 'cancel':
                # 원주문이 미체결 목록에서 사라지면 취소 확정
                open_rows = open_orders_by_account.get(t.account)
                if open_rows is None:
                    # fetch 실패 시 다음 폴링으로 미룸
                    continue
                still_open = any((r.get('ord_no') or '').strip() == t.orig_ord_no
                                 for r in open_rows)
                if not still_open:
                    self._handle_cancel_confirmed(t)
                elif elapsed >= UNFILLED_TIMEOUT_SECONDS:
                    self._handle_cancel_timeout(t)
                continue
            # fill kind (기본)
            rows = rows_by_account.get(t.account, [])
            row = next((r for r in rows if (r.get('ord_no') or '').strip() == ord_no), None)
            if row is not None:
                self._handle_row(t, row)
            elif elapsed >= UNFILLED_TIMEOUT_SECONDS:
                self._handle_timeout(t)

    def _handle_row(self, t: Tracked, row: dict) -> None:
        cntr_qty = int(row.get('cntr_qty') or 0)
        cntr_uv = int(row.get('cntr_uv') or 0)
        mdfy_cncl = (row.get('mdfy_cncl') or '').strip()

        if mdfy_cncl:
            # 사용자가 cancel_open_order 로 발주한 취소가 잡힌 거면, cancel watch 가
            # 확정 메시지를 보낸다 — 여기서는 사후거절 알림 억제 + 조용히 fill watch 해제.
            with self._lock:
                user_initiated = any(
                    x.kind == 'cancel' and x.orig_ord_no == t.ord_no
                    for x in self._tracked.values()
                )
                self._tracked.pop(t.ord_no, None)
            if user_initiated:
                ledger.append('canceled', {'card_id': t.card_id, 'ord_no': t.ord_no,
                                            'account': t.account, 'symbol': t.symbol,
                                            'reason': 'USER_CANCEL_VIA_CANCEL_ORDER'})
                return
            ledger.append('failed', {'card_id': t.card_id, 'ord_no': t.ord_no,
                                       'account': t.account, 'symbol': t.symbol,
                                       'reason': 'BROKER_POST_REJECT',
                                       'mdfy_cncl': mdfy_cncl})
            self._send(card.format_broker_post_reject(t.card_id, t.side, t.symbol_name,
                                                        t.ord_no, mdfy_cncl))
            return

        if cntr_qty > t.last_cntr_qty:
            new_fill = cntr_qty - t.last_cntr_qty
            t.last_cntr_qty = cntr_qty
            if cntr_qty >= t.order_qty:
                with self._lock:
                    self._tracked.pop(t.ord_no, None)
                ledger.append('filled', {'card_id': t.card_id, 'ord_no': t.ord_no,
                                          'account': t.account, 'symbol': t.symbol,
                                          'qty': cntr_qty, 'price': cntr_uv})
                self._send(card.format_filled(t.card_id, t.side, t.symbol_name,
                                               cntr_qty, cntr_uv, t.ord_no))
                # 전량 체결 → 자산웹 거래내역 즉시 갱신 (별도 스레드, 추적 블로킹 X)
                _spawn_journal_collect()
                _bump_fill_signal()  # 자산웹 패널 자동 새로고침(수량·예수금·당일정산)
            else:
                ledger.append('partial', {'card_id': t.card_id, 'ord_no': t.ord_no,
                                            'account': t.account, 'symbol': t.symbol,
                                            'cntr_qty': cntr_qty, 'order_qty': t.order_qty,
                                            'price': cntr_uv, 'new_fill': new_fill})
                self._send(card.format_partial(t.card_id, t.side, t.symbol_name,
                                                 cntr_qty, t.order_qty, cntr_uv, t.ord_no))
                _bump_fill_signal()  # 부분체결도 수량·예수금 변동 → 패널 새로고침

    def _handle_timeout(self, t: Tracked) -> None:
        with self._lock:
            self._tracked.pop(t.ord_no, None)
        ledger.append('expired', {'card_id': t.card_id, 'ord_no': t.ord_no,
                                    'account': t.account, 'symbol': t.symbol,
                                    'reason': 'FILL_WATCH_TIMEOUT',
                                    'order_qty': t.order_qty,
                                    'last_cntr_qty': t.last_cntr_qty})
        self._send(card.format_unfilled_timeout(t.card_id, t.side, t.symbol_name,
                                                  t.last_cntr_qty, t.order_qty, t.ord_no))

    def _handle_cancel_confirmed(self, t: Tracked) -> None:
        with self._lock:
            self._tracked.pop(t.ord_no, None)
            if t.orig_ord_no:
                self._tracked.pop(str(t.orig_ord_no), None)
        ledger.append('cancel_confirmed', {'card_id': t.card_id,
                                             'new_ord_no': t.ord_no,
                                             'orig_ord_no': t.orig_ord_no,
                                             'account': t.account, 'symbol': t.symbol,
                                             'cancel_qty': t.order_qty})
        self._send(card.format_cancel_confirmed(t.side, t.symbol_name,
                                                  t.orig_ord_no, t.ord_no, t.order_qty))

    def _handle_cancel_timeout(self, t: Tracked) -> None:
        with self._lock:
            self._tracked.pop(t.ord_no, None)
        ledger.append('cancel_unconfirmed_timeout', {'card_id': t.card_id,
                                                       'new_ord_no': t.ord_no,
                                                       'orig_ord_no': t.orig_ord_no,
                                                       'account': t.account,
                                                       'symbol': t.symbol,
                                                       'cancel_qty': t.order_qty})
        self._send(card.format_cancel_unconfirmed_timeout(t.side, t.symbol_name,
                                                            t.orig_ord_no, t.ord_no))


_watcher = _FillWatcher()


# ---------- 외부 진입점 ----------

def configure(send_func, fetch_executions, fetch_open_orders=None):
    _watcher.configure(send_func, fetch_executions, fetch_open_orders)


def watch(ord_no, account, side, symbol, symbol_name, order_qty, price, order_type, card_id):
    """알바 측 진입점 — 큐에 append + 데몬 ensure_running.

    in-memory 가 아니라 별도 데몬 프로세스가 추적. 호출 프로세스는 즉시 반환.
    """
    if not ord_no:
        return
    entry = {
        'ord_no': str(ord_no), 'account': account, 'side': side,
        'symbol': symbol, 'symbol_name': symbol_name,
        'order_qty': int(order_qty), 'price': price,
        'order_type': order_type, 'card_id': card_id,
        'started_at': time.time(), 'last_cntr_qty': 0,
        'kind': 'fill',
    }
    # 큐에 이미 있는 ord_no 면 중복 append 방지
    existing = read_queue()
    if any(e.get('ord_no') == entry['ord_no'] for e in existing):
        ensure_daemon_running()
        return
    append_queue_entry(entry)
    ensure_daemon_running()


def watch_cancel(new_ord_no, orig_ord_no, account, side, symbol, symbol_name,
                  cancel_qty, card_id=None):
    """취소 주문(kt10003) 접수 후 broker 확정 추적.

    new_ord_no: kt10003 응답의 ord_no (취소 주문 자체 번호 — _tracked dict key)
    orig_ord_no: 취소 대상 원주문 ord_no — ka10075 폴링으로 사라지는지 감시
    cancel_qty: 취소 요청 수량 (cancel_qty=0 호출 시 발주 시점 unfilled_qty 전달)
    side/symbol/symbol_name: 원주문의 것 (텔레그램 메시지 가독성용)
    """
    if not new_ord_no or not orig_ord_no:
        return
    entry = {
        'ord_no': str(new_ord_no), 'account': account, 'side': side,
        'symbol': symbol, 'symbol_name': symbol_name,
        'order_qty': int(cancel_qty), 'price': None,
        'order_type': 'CANCEL', 'card_id': card_id or '',
        'started_at': time.time(), 'last_cntr_qty': 0,
        'kind': 'cancel', 'orig_ord_no': str(orig_ord_no),
    }
    existing = read_queue()
    if any(e.get('ord_no') == entry['ord_no'] for e in existing):
        ensure_daemon_running()
        return
    append_queue_entry(entry)
    ensure_daemon_running()


# ---------- 테스트 헬퍼 ----------

def _reset_for_test():
    with _watcher._lock:
        _watcher._tracked.clear()
    if QUEUE_FILE.exists():
        try:
            QUEUE_FILE.unlink()
        except OSError:
            pass
    if PID_FILE.exists():
        try:
            PID_FILE.unlink()
        except OSError:
            pass


def _peek_for_test():
    with _watcher._lock:
        return dict(_watcher._tracked)
