"""
Fiscal printer service for dTEC100extra ΦΗΜ machines.

Protocol: plain TXT files dropped into an OUT folder. The fiscal driver reads
them, processes the receipt, then writes a reply into the IN folder.
Files must be encoded in CP737 (Greek) with CRLF line endings.
All item text must be UPPER CASE.

File naming: fp-YYMMDD-HHMM-NNNNNNN.txt  (NNNNNNN = zero-padded counter)
The driver deletes the command file after reading it and will never re-read the
same filename, so uniqueness is critical.
"""

import os
import json
import time
import threading
import logging
from datetime import datetime, timezone

logger = logging.getLogger(__name__)

# Shared counter — persisted only in-process; restarts reset to 0 but the
# date+time prefix keeps filenames unique across restarts.
_counter_lock = threading.Lock()
_counter = 0


def _next_counter() -> int:
    global _counter
    with _counter_lock:
        _counter += 1
        return _counter


def _unique_filename() -> str:
    now = datetime.now()
    date_part = now.strftime("%y%m%d")
    time_part = now.strftime("%H%M")
    seq = _next_counter()
    return f"fp-{date_part}-{time_part}-{seq:07d}.txt"


def _get_folders(db) -> tuple[str, str]:
    """Return (out_folder, in_folder) from pos_settings. Raises if not configured."""
    from models.settings import PosSettings
    rows = {
        r.key: r.value
        for r in db.query(PosSettings).filter(
            PosSettings.key.in_(["fiscal.out_folder", "fiscal.in_folder"])
        ).all()
    }
    out_folder = rows.get("fiscal.out_folder", "").strip()
    in_folder = rows.get("fiscal.in_folder", "").strip()
    if not out_folder or not in_folder:
        raise ValueError("Fiscal folder paths are not configured")
    return out_folder, in_folder


def _get_setting(db, key: str, default: str = "") -> str:
    from models.settings import PosSettings
    row = db.query(PosSettings).filter(PosSettings.key == key).first()
    return row.value if row else default


def is_fiscal_enabled(db) -> bool:
    return _get_setting(db, "fiscal.enabled", "false") == "true"


def _build_command_file(items: list[dict], payment_method: str, total: float, db) -> str:
    """
    Build the fiscal command file content as a string.

    items: list of {"name": str, "qty": int|float, "unit_price": float, "vat_group_id": int}
    payment_method: "cash" | "card"
    total: sum of all items (already computed by caller)
    """
    clerk_id = _get_setting(db, "fiscal.clerk_id", "2")
    eftpos_id = _get_setting(db, "fiscal.eftpos_id", "1")
    end_message_raw = _get_setting(db, "fiscal.end_message", "[]")
    try:
        end_lines: list[str] = json.loads(end_message_raw)
    except Exception:
        end_lines = []

    lines = ["FR"]

    for item in items:
        name = (item["name"] or "").upper().strip()
        qty = item["qty"]
        price = item["unit_price"]
        vat = item["vat_group_id"]
        # Quantity: integer if whole, otherwise 3 decimal places (kg/liter support)
        if isinstance(qty, float) and qty != int(qty):
            qty_str = f"{qty:.3f}"
        else:
            qty_str = str(int(qty))
        price_str = f"{price:.2f}"
        lines.append(f"SI|{name}|{qty_str}|{price_str}|{vat}")

    # End message lines (FM commands go BEFORE the close command)
    for i, msg_line in enumerate(end_lines, start=1):
        if msg_line.strip():
            lines.append(f"FM|{i}|{msg_line.upper()}")

    if payment_method == "card":
        total_str = f"{total:.2f}"
        lines.append(f"CD|{clerk_id}|{eftpos_id}|1|{total_str}")
    else:
        lines.append(f"CR|{clerk_id}")

    return "\r\n".join(lines) + "\r\n"


def _write_and_poll(items: list[dict], payment_method: str, total: float,
                    out_folder: str, in_folder: str, content: str,
                    filename: str, timeout_seconds: int) -> dict:
    """Write the command file and poll for a reply. Pure logic, no DB access."""
    out_path = os.path.join(out_folder, filename)
    reply_filename = f"answer{filename}"
    in_path = os.path.join(in_folder, reply_filename)

    try:
        with open(out_path, "w", encoding="cp737", errors="replace") as f:
            f.write(content)
    except OSError as e:
        return {"success": False, "filename": filename, "reply": None,
                "error": f"Failed to write fiscal file: {e}"}

    logger.info("Fiscal: wrote %s (%d bytes)", out_path, len(content))

    deadline = time.monotonic() + timeout_seconds
    reply_content = None
    while time.monotonic() < deadline:
        if os.path.exists(in_path):
            try:
                with open(in_path, "r", encoding="cp737", errors="replace") as f:
                    reply_content = f.read().strip()
                break
            except OSError:
                pass
        time.sleep(0.5)

    if reply_content is None:
        return {"success": False, "filename": filename, "reply": None,
                "error": f"Fiscal timeout: no reply after {timeout_seconds}s"}

    first_line = reply_content.splitlines()[0].strip() if reply_content else "1"
    success = first_line == "0"

    logger.info("Fiscal: reply for %s — success=%s content=%r", filename, success, reply_content)
    return {"success": success, "filename": filename, "reply": reply_content,
            "error": None if success else f"ΦΗΜ error: {reply_content}"}


def send_fiscal_receipt(items: list[dict], payment_method: str, total: float, db,
                        timeout_seconds: int = 30) -> dict:
    """
    Build and send a fiscal receipt, then block waiting for the reply.
    Returns: {"success": bool, "filename": str, "reply": str | None, "error": str | None}
    """
    out_folder, in_folder = _get_folders(db)
    os.makedirs(out_folder, exist_ok=True)
    os.makedirs(in_folder, exist_ok=True)
    filename = _unique_filename()
    content = _build_command_file(items, payment_method, total, db)
    return _write_and_poll(items, payment_method, total, out_folder, in_folder,
                           content, filename, timeout_seconds)


def send_fiscal_receipt_background(items: list[dict], payment_method: str, total: float,
                                   db, order_id: int, db_factory,
                                   timeout_seconds: int = 60) -> None:
    """
    Write the fiscal file and poll for the reply in a background thread.
    Updates order.fiscal_status in the DB when done (success or failure/timeout).

    db_factory: a callable () -> Session (e.g. SessionLocal) so the thread can
                open its own DB session independently of the request session.
    """
    try:
        out_folder, in_folder = _get_folders(db)
        os.makedirs(out_folder, exist_ok=True)
        os.makedirs(in_folder, exist_ok=True)
        filename = _unique_filename()
        content = _build_command_file(items, payment_method, total, db)
    except Exception as e:
        logger.error("Fiscal background setup failed for order %s: %s", order_id, e)
        _update_fiscal_status(order_id, "failed", db_factory)
        return

    def _run():
        result = _write_and_poll(items, payment_method, total, out_folder, in_folder,
                                 content, filename, timeout_seconds)
        status = "success" if result["success"] else "failed"
        _update_fiscal_status(order_id, status, db_factory)

    thread = threading.Thread(target=_run, daemon=True)
    thread.start()


def _update_fiscal_status(order_id: int, status: str, db_factory) -> None:
    """Open a fresh DB session and set order.fiscal_status."""
    try:
        session = db_factory()
        try:
            from models.order import Order
            order = session.query(Order).filter(Order.id == order_id).first()
            if order:
                order.fiscal_status = status
                session.commit()
                logger.info("Fiscal: order %s fiscal_status → %s", order_id, status)
        finally:
            session.close()
    except Exception as e:
        logger.error("Fiscal: failed to update fiscal_status for order %s: %s", order_id, e)


def validate_items_for_fiscal(order_items, db) -> list[str]:
    """
    Check that every non-cancelled item in the list has a fiscal_vat_group_id.
    Returns a list of product names that are missing it (empty = all OK).
    """
    missing = []
    for item in order_items:
        product = item.product
        if product is None:
            missing.append(f"#product_{item.product_id}")
            continue
        vat_group = getattr(product, "fiscal_vat_group_id", None)
        if not vat_group:
            missing.append(product.name)
    return missing


def build_fiscal_items(order_items) -> list[dict]:
    """
    Convert a list of OrderItem objects into the dicts that send_fiscal_receipt expects.
    Uses fiscal_name if set, else falls back to product.name.
    Applies price_adjustment to the unit price.
    """
    result = []
    for item in order_items:
        product = item.product
        name = (getattr(product, "fiscal_name", None) or "").strip() or (product.name if product else f"#{item.product_id}")
        adj = getattr(item, "price_adjustment", 0.0) or 0.0
        unit_price = item.unit_price + adj
        result.append({
            "name": name,
            "qty": item.quantity,
            "unit_price": unit_price,
            "vat_group_id": product.fiscal_vat_group_id,
        })
    return result


def test_folders(out_folder: str, in_folder: str) -> dict:
    """
    Verify the two folders are accessible (read + write).
    Returns {"ok": bool, "out_folder": str|None, "in_folder": str|None}
    where each value is None if accessible, or an error string if not.
    """
    def check(path):
        try:
            os.makedirs(path, exist_ok=True)
            test_file = os.path.join(path, ".fiscal_test")
            with open(test_file, "w") as f:
                f.write("test")
            os.remove(test_file)
            return None
        except Exception as e:
            return str(e)

    out_err = check(out_folder)
    in_err = check(in_folder)
    return {
        "ok": out_err is None and in_err is None,
        "out_folder_error": out_err,
        "in_folder_error": in_err,
    }
