"""
浏览器自动化填报

使用 Playwright 操作财务报销系统，自动完成登录、填单、上传附件等操作。

对外接口:
    run_bot(config, invoices)                           启动浏览器并执行填报流程
"""

import json
from pathlib import Path
from typing import Any

from . import get_logger

log = get_logger("bot")


# ------------------------------------------------------------------
# 日期格式化
# ------------------------------------------------------------------


def _format_date(date_str: str) -> str:
    """将 '2026/5/13' 或 '2026-5-13' 转为 '2026-05-13'"""
    if not date_str:
        return ""
    parts = date_str.replace("-", "/").split("/")
    if len(parts) == 3:
        return f"{parts[0].zfill(4)}-{parts[1].zfill(2)}-{parts[2].zfill(2)}"
    return date_str


def _classify_invoice_batch(
    invoices: list[dict[str, str]],
) -> dict[str, list[dict[str, str]]]:
    """按发票类型分组"""
    travel: list[dict[str, str]] = []
    general: list[dict[str, str]] = []
    application: list[dict[str, str]] = []
    for inv in invoices:
        inv_type = inv.get("invoice_type", "general")
        if inv_type == "application":
            application.append(inv)
        elif inv_type in ("train", "hotel"):
            travel.append(inv)
        else:
            general.append(inv)
    return {"travel": travel, "general": general, "application": application}


# ------------------------------------------------------------------
# 报销机器人
# ------------------------------------------------------------------


class ReimburseBot:
    """财务报销自动化机器人"""

    def __init__(self, config: dict[str, Any], headless: bool = False, work_dir: Path | None = None):
        self.config = config
        self.headless = headless
        self.work_dir: Path | None = None
        self.browser: Any = None
        self.context: Any = None
        self.page: Any = None

        from playwright.sync_api import sync_playwright

        self._pw_ctx = sync_playwright()
        self.pw = self._pw_ctx.__enter__()

    def launch(self) -> None:
        """启动浏览器"""
        self.browser = self.pw.chromium.launch(headless=self.headless)
        self.context = self.browser.new_context(viewport={"width": 1360, "height": 768})
        self.page = self.context.new_page()
        self.page.set_default_timeout(30000)

    def login_portal(self) -> None:
        """登录信息门户"""
        log.info("登录信息门户...")

        self.page.goto(self.config["sso_login_url"], wait_until="domcontentloaded")
        self._wait_for('text="微信扫码登录"', timeout=5000)

        try:
            self.page.fill('input[placeholder*="工号"], input[placeholder*="学号"]', self.config["username"])
            self.page.fill('input[placeholder*="密码"]', self.config["password"])
        except Exception:
            log.warning("未找到登录输入框，可能已登录")

        try:
            checkbox = self.page.query_selector('input[type="checkbox"]')
            if checkbox and not checkbox.is_checked():
                checkbox.click()
        except Exception:
            pass

        for selector in ['button:has-text("登录")', 'input[value="登录"]', 'text="登录"']:
            try:
                self.page.click(selector, timeout=3000)
                break
            except Exception:
                continue

        self._wait_for_portal()

    def _wait_for_portal(self) -> None:
        """等待跳转到统一信息平台"""
        for _ in range(30):
            self.page.wait_for_timeout(1000)
            url = self.page.url
            if any(kw in url for kw in ("tyrz.fynu.edu.cn/zs-uip", "tyrz.fynu.edu.cn/oshall", "portal")):
                self._screenshot("portal_loaded")
                return
        log.error("等待门户跳转超时")
        self._screenshot("portal_timeout")
        raise TimeoutError("登录超时，未跳转到信息门户")

    def navigate_to_reimburse(self, page_key: str = "reimburse_page") -> None:
        """从统一信息平台进入报销系统"""
        log.info("进入报销系统...")
        self._wait_for('text="快捷入口"', timeout=5000)

        try:
            self.page.click('text="财务系统"', timeout=5000)
        except Exception:
            log.warning("未找到财务系统入口")

        new_tab = None
        for _ in range(15):
            self.page.wait_for_timeout(1000)
            for p in self.context.pages:
                if "dddl" in p.url or "210.45.32.214" in p.url:
                    new_tab = p
                    break
            if new_tab:
                break

        if new_tab:
            self.page = new_tab
            self._wait_for('text="网络报销"', timeout=5000)
        else:
            log.warning(f"未找到单点登录页面，当前 URL: {self.page.url}")

        for p in self.context.pages[:-1]:
            try:
                p.close()
            except Exception:
                pass

        try:
            link = self.page.query_selector('a:has(img[src*="wlbx"])')
            if link:
                reimburse_url = link.get_attribute("href")
                self.page.goto(reimburse_url, wait_until="domcontentloaded", timeout=15000)
        except Exception:
            pass

        self._wait_for('text="报销录入"', timeout=5000)
        common_url = self.config["reimburse_url"] + self.config[page_key]
        self.page.goto(common_url, wait_until="domcontentloaded", timeout=15000)
        self._wait_for('text="单据状态:"', timeout=5000)

    def create_new_form(self) -> None:
        """点击「新增」创建新单据"""
        log.info("创建新单据...")
        self.page.wait_for_timeout(2000)

        try:
            self.page.click("#insert", timeout=5000)
        except Exception:
            try:
                self.page.click("text=新增", timeout=3000)
            except Exception as err:
                self._screenshot("no_add_button")
                raise RuntimeError("无法点击新增按钮") from err

        self.page.wait_for_timeout(3000)
        self._screenshot("after_add_click")

    def fill_travel_info(self, basic_info: dict[str, Any]) -> None:
        """填写差旅报销基本信息"""
        log.info("填写差旅报销信息...")

        try:
            self.page.fill("#CAUSE", basic_info.get("travel_purpose", ""))
            self.page.fill("#SITE", basic_info.get("travel_location", ""))

            self.page.click("#PROJECTCODE", timeout=10000)
            self.page.wait_for_timeout(1000)
            self.page.wait_for_selector("#promodal .fixed-table-body tbody tr", timeout=10000)
            first_row = self.page.query_selector("#promodal .fixed-table-body tbody tr")
            if first_row:
                first_row.click()
                self.page.wait_for_timeout(1000)

            self.page.fill("#THEKSRQ", _format_date(basic_info.get("start_date", "")))
            self.page.fill("#THEJSRQ", _format_date(basic_info.get("end_date", "")))
            self.page.click("#saveAndNext", timeout=5000)
            self.page.wait_for_timeout(2000)
            self._screenshot("travel_basic_done")
        except Exception:
            log.error("填写基本信息失败")
            self._screenshot("travel_basic_error")

    def fill_basic_info(self, description: str = "元器件采购报销") -> None:
        """填写基本信息"""
        log.info("填写基本信息...")

        try:
            self.page.fill("#EXPENEXPLAIN", description)
        except Exception:
            pass

        try:
            self.page.click("#PROJECTCODE", timeout=10000)
            self.page.wait_for_timeout(1000)
        except Exception:
            pass

        self._screenshot("step3_project_modal")

        try:
            self.page.wait_for_selector("#promodal .fixed-table-body tbody tr", timeout=10000)
            first_row = self.page.query_selector("#promodal .fixed-table-body tbody tr")
            if first_row:
                first_row.click()
                self.page.wait_for_timeout(1000)
        except Exception:
            pass

        self._screenshot("step3_project_selected")

        try:
            self.page.click("#saveAndNext", timeout=5000)
            self.page.wait_for_timeout(2000)
        except Exception:
            pass

        self._screenshot("step3_done")

    def add_travel_items(self, travel_items: dict[str, Any]) -> None:
        """录入差旅报销明细"""
        vehicle_map = {
            "火车": "01",
            "汽车": "02",
            "轮船": "03",
            "自带车": "04",
            "公务车": "05",
            "飞机": "06",
            "租车": "07",
            "自驾车": "08",
        }
        try:
            traffic_info = travel_items.get("transport_fee") or []
            for item in traffic_info:
                self.page.click("#insertDetail", timeout=5000)
                self._wait_for('text="增加明细"', timeout=5000)
                self.page.select_option("#cost", "1")
                self.page.wait_for_timeout(500)
                vehicle = item.get("vehicle_type", "")
                if vehicle in vehicle_map:
                    self.page.select_option("#jtgj", vehicle_map[vehicle])

                self.page.fill("#ksdd", item.get("departure_place", ""))
                self.page.fill("#jsdd", item.get("arrival_place", ""))
                self.page.fill(
                    '#t1 input[name="expenPwTraveldetail.MONEY"]',
                    str(item.get("amount", "")),
                )
                self.page.fill(
                    '#t1 input[name="expenPwTraveldetail.HOWBILL"]',
                    str(item.get("bill_count", "")),
                )
                self.page.fill(
                    '#t1 input[name="expenPwTraveldetail.SMARK"]',
                    str(item.get("remark", "")),
                )
                self.page.click("#detailAdd", timeout=3000)
                self.page.wait_for_timeout(1000)

            hotel_info = travel_items.get("hotel_fee") or []
            for item in hotel_info:
                self.page.click("#insertDetail", timeout=5000)
                self._wait_for('text="增加明细"', timeout=5000)
                self.page.select_option("#cost", "2")
                self.page.wait_for_timeout(500)
                self.page.fill("#ksrq2", _format_date(str(item.get("checkin_date", ""))))
                self.page.fill("#jsrq2", _format_date(str(item.get("checkout_date", ""))))
                self.page.fill("#ts2", str(item.get("days", "")))
                self.page.fill("#rs2", str(item.get("person_count", "")))
                self.page.fill(
                    '#t2 input[name="expenPwTraveldetail.FPMONEY"]',
                    str(item.get("invoice_amount", "")),
                )
                self.page.fill(
                    '#t2 input[name="expenPwTraveldetail.MONEY"]',
                    str(item.get("reimburse_amount", "")),
                )
                self.page.fill(
                    '#t2 input[name="expenPwTraveldetail.SMARK"]',
                    str(item.get("remark", "")),
                )
                self.page.click("#detailAdd", timeout=3000)
                self.page.wait_for_timeout(1000)

            conference_info = travel_items.get("conference_fee") or []
            for item in conference_info:
                self.page.click("#insertDetail", timeout=5000)
                self._wait_for('text="增加明细"', timeout=5000)
                self.page.select_option("#cost", "3")
                self.page.wait_for_timeout(500)
                self.page.fill(
                    '#t3 input[name="expenPwTraveldetail.HOWBILL"]',
                    str(item.get("bill_count", "")),
                )
                self.page.fill(
                    '#t3 input[name="expenPwTraveldetail.MONEY"]',
                    str(item.get("amount", "")),
                )
                self.page.fill(
                    '#t3 input[name="expenPwTraveldetail.SMARK"]',
                    str(item.get("remark", "")),
                )
                self.page.click("#detailAdd", timeout=3000)
                self.page.wait_for_timeout(1000)
        except Exception as e:
            log.error(f"录入总明细失败: {e}")
            self._screenshot("item_total_error")
            raise

    def add_reimburse_items(self, invoices: list[dict[str, Any]]) -> None:
        """录入报销明细（一条总明细）"""
        card_amount = sum(inv["card_amount"] for inv in invoices)
        log.info(f"录入报销明细 (合计 ¥{card_amount:.2f})...")

        try:
            self.page.click("#insertDetail", timeout=5000)
            self._wait_for('text="经济事项名称"', timeout=5000)
            self.page.click("#economicscode2")
            self.page.wait_for_timeout(1000)

            try:
                self.page.wait_for_selector("#econmodal .fixed-table-body tbody tr", timeout=10000)
                rows = self.page.query_selector_all("#econmodal .fixed-table-body tbody tr")
                if len(rows) >= 3:
                    rows[2].click()
                    self.page.wait_for_timeout(1000)
            except Exception:
                pass

            self.page.fill('input[name="expenPwCommondetail.HOWBILLS"]', f"{len(invoices)}")
            self.page.fill("#je_zwzcdz", f"{card_amount:.2f}")
            self.page.click("#detailAdd", timeout=3000)
            self.page.wait_for_timeout(1000)

            self._screenshot("item_total")
        except Exception as e:
            log.error(f"录入总明细失败: {e}")
            self._screenshot("item_total_error")
            raise

    def fill_travel_payment(self, payment_info: list[dict[str, Any]]) -> None:
        """录入差旅支付信息"""
        log.info("录入差旅支付信息...")
        try:
            self.page.click('text="下一步(支付方式)"', timeout=5000)
            self._wait_for('text="下一步(补助清单)"', timeout=5000)

            for info in payment_info:
                self.page.click("#insertPay", timeout=5000)
                self.page.wait_for_timeout(1000)
                self.page.fill("#personid2", self.config["default_name"])
                self.page.fill("#accountname2", self.config["default_person_id"])
                self.page.fill("#receiptdate2", _format_date(info["card_date"]))
                self.page.fill("#localaccount2", self.config["default_card_no"])
                self.page.fill("#receiptmoney2", str(info["card_amount"]))
                self.page.fill("#money2", str(info["card_amount"]))
                self.page.fill("#merchant2", info.get("merchant", ""))
                self.page.fill("#smark2", info.get("remark", ""))
                self.page.click("#payAdd", timeout=3000)
                self.page.wait_for_timeout(1000)
        except Exception as e:
            log.error(f"支付方式录入失败: {e}")
            self._screenshot("step5_error")
            raise

        self._screenshot("step5_done")

    def fill_payment(self, invoices: list[dict[str, Any]]) -> None:
        """录入支付信息"""
        log.info("录入支付信息...")
        try:
            self.page.click('text="下一步(支付方式)"', timeout=5000)
            self._wait_for('text="下一步(附件清单)"', timeout=5000)

            for inv in invoices:
                self.page.click("#insertPay", timeout=5000)
                self.page.wait_for_timeout(1000)
                self.page.fill("#personid2", inv["person_id"])
                self.page.fill("#accountname2", inv["person_name"])
                self.page.fill("#receiptdate2", inv["card_date"])
                self.page.fill("#localaccount2", inv["card_no"])
                self.page.fill("#receiptmoney2", str(inv["card_amount"]))
                self.page.fill("#money2", str(inv["card_amount"]))
                self.page.fill("#merchant2", inv["seller_name"])
                self.page.fill("#smark2", inv["remark"])
                self.page.click("#payAdd", timeout=3000)
                self.page.wait_for_timeout(1000)
        except Exception as e:
            log.error(f"支付方式录入失败: {e}")
            self._screenshot("step5_error")
            raise

        self._screenshot("step5_done")

    def upload_attachments(self, invoices: list[dict[str, Any]]) -> None:
        """上传附件"""
        log.info("上传附件...")
        try:
            self.page.click("#next4", timeout=5000)
            self._wait_for("#submit2", timeout=5000)

            attachment_files = sorted((self.work_dir or Path(__file__).parent.parent).glob("*.pdf"))
            if not attachment_files:
                log.warning("未找到附件 PDF，跳过附件上传")
                return

            for i, inv in enumerate(invoices):
                file_path = attachment_files[i] if i < len(attachment_files) else None
                try:
                    self._wait_for("#insertAcc", timeout=20000)
                    self.page.click("#insertAcc", timeout=5000)
                    self._wait_for("#fjlx", timeout=5000)
                    self.page.select_option("#fjlx", "1")
                    explanation = f"{inv['item_name']} - {inv['invoice_no']}"
                    self.page.fill("#fpsmxx", explanation)
                except Exception:
                    pass

                if file_path and file_path.exists():
                    try:
                        self.page.set_input_files("#file", str(file_path))
                        self.page.wait_for_timeout(1000)
                    except Exception as e:
                        log.error(f"文件上传失败: {e}")
                try:
                    self.page.click("#cjtj", timeout=5000)
                except Exception:
                    try:
                        self.page.press("body", "Escape")
                    except Exception:
                        pass
            log.info("上传附件完成")
        except Exception as e:
            log.error(f"附件上传失败: {e}")
            self._screenshot("step6_error")
            raise

        self._screenshot("step6_done")

    def upload_travel_attachments(self, attachment_info: list[dict[str, Any]]) -> None:
        """上传差旅附件"""
        log.info("上传差旅附件...")
        try:
            self.page.click("#next4", timeout=5000)
            self._wait_for("#submit2", timeout=5000)

            for info in attachment_info:
                attachment_file = self.work_dir / info["filename"]
                self._wait_for("#insertAcc", timeout=20000)
                self.page.click("#insertAcc", timeout=5000)
                self._wait_for("#fjlx", timeout=5000)
                if info["attachment_type"] == "invoice":
                    self.page.select_option("#fjlx", "1")
                else:
                    self.page.select_option("#fjlx", "2")
                self.page.fill("#fpsmxx", info["attachment_desc"])
                if attachment_file and attachment_file.exists():
                    self.page.set_input_files("#file", str(attachment_file))
                    self.page.wait_for_timeout(1000)
                    self.page.click("#cjtj", timeout=5000)
            log.info("上传差旅附件完成")
        except Exception as e:
            log.error(f"差旅附件上传失败: {e}")
            self._screenshot("travel_attachment_error")
            raise
        self._screenshot("travel_attachment_done")

    def close(self) -> None:
        """关闭浏览器"""
        if self.context:
            self.context.close()
        if self.browser:
            self.browser.close()
        try:
            self._pw_ctx.__exit__(None, None, None)
        except Exception:
            pass

    def _wait_for(self, selector: str, timeout: int | None = None) -> None:
        self.page.wait_for_selector(selector, timeout=timeout)

    def _screenshot(self, name: str) -> None:
        img_dir = Path(__file__).parent.parent / "images"
        img_dir.mkdir(exist_ok=True)
        self.page.screenshot(path=str(img_dir / f"debug_{name}.png"))

    def fill_travel_subsidy(self, subsidy_info: list[dict[str, Any]]) -> None:
        """录入差旅补助清单"""
        log.info("录入差旅补助清单...")
        try:
            self.page.click("#next3", timeout=5000)
            self._wait_for("#next4", timeout=5000)

            for info in subsidy_info:
                self.page.click("#insertSubsidy", timeout=5000)
                self._wait_for('text="增加补助清单"', timeout=5000)
                self.page.click("#jzg3", timeout=5000)
                self.page.wait_for_timeout(500)
                if info["person_name"] and info["person_name"] != "":
                    self.page.fill("#seacher", info["person_name"])
                elif info["person_id"] and info["person_id"] != "":
                    self.page.fill("#seacher", info["person_id"])
                else:
                    raise ValueError(f"人员编号和人员姓名不能同时为空: {info}")
                self.page.click("#cx", timeout=5000)
                self.page.wait_for_selector("div.fixed-table-loading", state="hidden", timeout=10000)
                self.page.click("#tableEmp tbody tr", timeout=10000)
                self.page.wait_for_timeout(1000)

                open_bank = self.page.input_value("#openbank1")
                if not open_bank:
                    log.info("员工开户行未填写，默认填写中国工商银行")
                    self.page.fill("#openbank1", "中国工商银行")

                self.page.fill("#startdate1", _format_date(info["start_date"]))
                self.page.fill("#enddate1", _format_date(info["end_date"]))
                self.page.fill("#trafficdays1", str(info["days"]))
                self.page.fill("#fooddays1", str(info["days"]))
                self.page.fill("#trafficnorm1", str(80))
                self.page.fill("#foodnorm1", str(100))
                trafficmoney = int(info["days"]) * 80
                foodmoney = int(info["days"]) * 100
                subsidymoney = trafficmoney + foodmoney
                self.page.fill("#trafficmoney1", str(trafficmoney))
                self.page.fill("#foodmoney1", str(foodmoney))
                self.page.fill("#subsidymoney1", str(subsidymoney))
                self.page.click("#add", timeout=3000)
                self.page.wait_for_timeout(1000)
        except Exception as e:
            log.error(f"差旅补助清单录入失败: {e}")
            self._screenshot("subsidy_error")
            raise

        self._screenshot("subsidy_done")


# ------------------------------------------------------------------
# 对外入口
# ------------------------------------------------------------------


def run_bot(config: dict[str, Any], headless: bool = False, work_dir: Path | None = None) -> None:
    """执行完整的浏览器填报流程，根据发票类型自动路由"""
    if not config["username"] or not config["password"]:
        raise ValueError("缺少用户名或密码")

    if not work_dir:
        raise ValueError("缺少工作目录")

    from .doc.llm_extractor import load_cache

    cache_map = load_cache(work_dir)
    doc_values = [v for k, v in cache_map.items() if k != "travel_info"]
    invoices = [data for data in doc_values if data.get("invoice_type") not in ("application", "payment")]
    applications = [data for data in doc_values if data.get("invoice_type") == "application"]

    groups = _classify_invoice_batch(invoices)
    travel_invoices = groups["travel"]
    general_invoices = groups["general"]

    log.info(
        f"发票分类: 差旅 {len(travel_invoices)} 张, 普通 {len(general_invoices)} 张, "
        f"出差事前申请单 {len(applications)} 份"
    )

    if travel_invoices and general_invoices:
        raise ValueError(
            f"发票类型混合（差旅 {len(travel_invoices)} 张 + 普通 {len(general_invoices)} 张），不支持混报，请分开提交"
        )

    is_travel = bool(travel_invoices)
    travel_info: dict[str, Any] = cache_map.get("travel_info", {})
    if is_travel and not travel_info:
        from .doc.llm_extractor import CACHE_DIR_NAME, extract_travel_info

        travel_info = extract_travel_info(source_dir=work_dir)
        cache_dir = work_dir / CACHE_DIR_NAME
        cache_dir.mkdir(parents=True, exist_ok=True)
        with open(cache_dir / "travel_info.json", "w", encoding="utf-8") as f:
            json.dump(travel_info, f, ensure_ascii=False, indent=2)
        log.info("差旅信息已保存到缓存")

    bot = ReimburseBot(config, headless=headless, work_dir=work_dir)
    bot.work_dir = work_dir
    try:
        bot.launch()
        bot.login_portal()

        if is_travel:
            log.info("处理差旅发票...")
            bot.navigate_to_reimburse(page_key="travel_page")
            bot.create_new_form()
            basic_info = travel_info["basic_info"]
            bot.fill_travel_info(basic_info)
            travel_items = travel_info["reimbursement_details"]
            bot.add_travel_items(travel_items)
            payment_info = travel_info["payment_methods"]
            bot.fill_travel_payment(payment_info)
            subsidy_info = travel_info["subsidy_list"]
            bot.fill_travel_subsidy(subsidy_info)
            attachment_info = travel_info["attachments"]
            bot.upload_travel_attachments(attachment_info)
        else:
            log.info("处理普通发票...")
            bot.navigate_to_reimburse(page_key="reimburse_page")
            bot.create_new_form()
            bot.fill_basic_info()
            bot.add_reimburse_items(general_invoices)
            bot.fill_payment(general_invoices)
            bot.upload_attachments(general_invoices)

    except Exception as e:
        log.error(f"操作失败: {e}")
        try:
            bot._screenshot("error")
        except Exception:
            pass
        raise
    finally:
        bot.close()


def run_bot_web(config: dict[str, Any], work_dir: Path) -> None:
    """Web 模式填报 — headless，附件从指定目录读取"""
    run_bot(config, headless=True, work_dir=work_dir)
