from __future__ import annotations

import re
from datetime import date
from decimal import Decimal

from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.models.employee import Employee
from app.schemas.reimbursement import (
    TravelReimbursementCalculatorRequest,
    TravelReimbursementCalculatorResponse,
)
from app.services.application_location_semantics import validate_application_location_text
from app.services.expense_claims import ExpenseClaimService
from app.services.expense_rule_runtime import ExpenseRuleRuntimeService, RuntimeTravelPolicy
from app.services.tenant_registry import required_tenant_id
from app.services.travel_policy_grades import travel_policy_grade_key_candidates
from app.services.travel_reimbursement_regions import (
    AMBIGUOUS_PROVINCE_CITY_NAMES,
    OTHER_REGION_LOCATION_KEYWORDS,
    OTHER_REGION_PROVINCE_KEYWORDS,
)
from app.services.user_agent_application_locations import normalize_application_location


class TravelReimbursementCalculatorService:
    def __init__(self, db: Session) -> None:
        self.db = db

    def calculate(
        self,
        payload: TravelReimbursementCalculatorRequest,
        current_user: CurrentUserContext,
    ) -> TravelReimbursementCalculatorResponse:
        days = max(1, int(payload.days))
        raw_location = str(payload.location or "").strip()
        if not raw_location:
            raise ValueError("请先填写出差地点。")
        location = normalize_application_location(raw_location) or raw_location
        location_error = validate_application_location_text(location)
        if location_error:
            raise ValueError(f"{location_error}请填写真实出差地点后再计算。")

        policy = self._load_travel_policy(current_user)
        grade = self._resolve_grade(payload.grade, current_user)
        if not grade:
            raise ValueError("未识别到当前员工职级，请在个人信息中维护职级后再计算。")

        grade_band = ExpenseClaimService._resolve_travel_policy_band(grade)
        if not grade_band:
            raise ValueError(f"当前职级 {grade} 暂未匹配到差旅报销职级。")

        matched_city = self._resolve_city(location, policy)
        matched_other_region = "" if matched_city else self._resolve_other_region(location)
        if not matched_city and not matched_other_region:
            raise ValueError(
                f"出差地点“{location}”未识别为有效出差地区，请按真实省市或规则表地点重新填写。"
            )
        city_tier = policy.city_tiers.get(matched_city, "tier_3") if matched_city else "tier_3"
        hotel_rate = self._resolve_hotel_rate(
            policy,
            grade_band,
            matched_city,
            city_tier,
            payload.travel_date,
        )
        allowance_region = self._resolve_allowance_region(
            location,
            matched_city or matched_other_region,
        )
        meal_rate = self._resolve_allowance_rate(policy, "meal", allowance_region)
        basic_rate = self._resolve_allowance_rate(policy, "basic", allowance_region)
        total_allowance_rate = self._resolve_total_allowance_rate(
            policy,
            allowance_region,
            meal_rate,
            basic_rate,
        )
        origin_city = self._resolve_origin_city(payload, current_user, policy)
        transport_mode = self._normalize_transport_mode(payload.transport_mode)
        transport_estimate = self._resolve_transport_estimate(
            policy,
            origin_city=origin_city,
            destination_city=matched_city or matched_other_region,
            destination_text=location,
            transport_mode=transport_mode,
        )
        transport_estimated_amount = Decimal(
            transport_estimate.get("amount") or Decimal("0.00")
        ).quantize(Decimal("0.01"))

        hotel_amount = hotel_rate * Decimal(days)
        allowance_amount = total_allowance_rate * Decimal(days)
        total_amount = hotel_amount + allowance_amount + transport_estimated_amount
        band_label = policy.band_labels.get(grade_band, grade_band)
        rule_name = policy.standard_rule_name or policy.rule_name or "公司差旅费报销规则"
        rule_version = policy.standard_rule_version or policy.rule_version or ""
        display_city = matched_city or self._format_other_region_display(matched_other_region)
        if transport_estimated_amount > Decimal("0.00"):
            formula_text = (
                f"交通 {self._format_money(transport_estimated_amount)} + "
                f"住宿 {self._format_money(hotel_rate)} × {days} 天 + "
                f"补贴 {self._format_money(total_allowance_rate)} × {days} 天 = "
                f"{self._format_money(total_amount)}"
            )
            summary_tail = (
                f"交通费用按“{transport_estimate.get('basis') or '交通费用预估表'}”"
                f"预估 {self._format_money(transport_estimated_amount)} 元。"
                f"按 {days} 天计算，住宿合计 {self._format_money(hotel_amount)} 元，"
                f"补贴合计 {self._format_money(allowance_amount)} 元，"
                f"申请预算占用参考总金额为 {self._format_money(total_amount)} 元。"
            )
        else:
            formula_text = (
                f"住宿 {self._format_money(hotel_rate)} × {days} 天 + "
                f"补贴 {self._format_money(total_allowance_rate)} × {days} 天 = "
                f"{self._format_money(total_amount)}"
            )
            summary_tail = (
                f"按 {days} 天计算，住宿合计 {self._format_money(hotel_amount)} 元，"
                f"补贴合计 {self._format_money(allowance_amount)} 元，"
                f"参考可报销总金额为 {self._format_money(total_amount)} 元。"
            )
        summary_text = (
            f"按《{rule_name}》{f'（{rule_version}）' if rule_version else ''}测算："
            f"当前职级 {grade} 对应 {band_label}，出差地点“{location}”匹配为“{display_city}”，"
            f"住宿标准 {self._format_money(hotel_rate)} 元/天，补贴区域为“{allowance_region}”，"
            f"补贴标准 {self._format_money(total_allowance_rate)} 元/天"
            f"（伙食 {self._format_money(meal_rate)} + 基本 {self._format_money(basic_rate)}）。"
            f"{summary_tail}"
        )

        return TravelReimbursementCalculatorResponse(
            days=days,
            location=location,
            matched_city=display_city,
            city_tier=city_tier,
            grade=grade,
            grade_band=grade_band,
            grade_band_label=band_label,
            hotel_rate=hotel_rate,
            hotel_amount=hotel_amount,
            allowance_region=allowance_region,
            meal_allowance_rate=meal_rate,
            basic_allowance_rate=basic_rate,
            total_allowance_rate=total_allowance_rate,
            allowance_amount=allowance_amount,
            transport_mode=(
                transport_mode or str(transport_estimate.get("transport_mode") or "").strip()
            ),
            transport_origin=str(
                transport_estimate.get("origin_city") or origin_city or ""
            ).strip(),
            transport_destination=str(
                transport_estimate.get("destination_city") or display_city or location
            ).strip(),
            transport_estimated_amount=transport_estimated_amount,
            transport_estimate_basis=str(transport_estimate.get("basis") or "").strip(),
            transport_estimate_confidence=str(transport_estimate.get("confidence") or "").strip(),
            transport_estimate_source=str(transport_estimate.get("source") or "").strip(),
            transport_estimate_rule_code=str(policy.transport_estimate_rule_code or "").strip(),
            transport_estimate_rule_name=str(policy.transport_estimate_rule_name or "").strip(),
            transport_estimate_rule_version=str(
                policy.transport_estimate_rule_version or ""
            ).strip(),
            travel_date=payload.travel_date,
            total_amount=total_amount,
            rule_name=rule_name,
            rule_version=rule_version,
            formula_text=formula_text,
            summary_text=summary_text,
        )

    def _load_travel_policy(self, current_user: CurrentUserContext) -> RuntimeTravelPolicy:
        # 规则资产由启动阶段初始化；计算器必须保持只读，不能在报销事务中触发规则同步提交。
        policy = (
            ExpenseRuleRuntimeService(
                self.db,
                tenant_id=current_user.tenant_id,
            )
            .load_catalog()
            .travel_policy
        )
        if policy is None:
            raise ValueError("规则中心暂未配置差旅报销规则。")
        return policy

    def _resolve_grade(
        self,
        grade: str | None,
        current_user: CurrentUserContext,
    ) -> str:
        normalized_grade = str(grade or "").strip()
        if normalized_grade:
            return normalized_grade

        employee = self._resolve_current_employee(current_user)
        if employee is not None and str(employee.grade or "").strip():
            return str(employee.grade).strip()
        return ""

    @staticmethod
    def _resolve_other_region(location: str) -> str:
        normalized = re.sub(r"\s+", "", str(location or "").strip())
        if not normalized:
            return ""
        if any(keyword in normalized for keyword in ("国外", "境外", "海外")):
            return "国外"
        for keyword in ("香港", "澳门", "台湾", "港澳台", "西藏", "拉萨", "新疆", "乌鲁木齐"):
            if keyword in normalized:
                return keyword
        city_matches = []
        province_matches = []
        for keyword in OTHER_REGION_LOCATION_KEYWORDS:
            if not keyword or keyword not in normalized:
                continue
            if keyword in OTHER_REGION_PROVINCE_KEYWORDS:
                province_matches.append(keyword)
            else:
                city_matches.append(keyword)
        candidates = city_matches or province_matches
        if candidates:
            return sorted(candidates, key=len, reverse=True)[0]
        return ""

    @staticmethod
    def _format_other_region_display(region: str) -> str:
        normalized = str(region or "").strip()
        if not normalized:
            return ""
        if normalized in {
            "国外",
            "香港",
            "澳门",
            "台湾",
            "港澳台",
            "西藏",
            "拉萨",
            "新疆",
            "乌鲁木齐",
        }:
            return normalized
        return f"{normalized}（其他地区）"

    def _resolve_current_employee(self, current_user: CurrentUserContext) -> Employee | None:
        tenant_id = required_tenant_id(current_user.tenant_id)
        candidates = [
            str(current_user.employee_id or "").strip(),
            str(current_user.username or "").strip(),
            str(current_user.name or "").strip(),
            str(current_user.employee_no or "").strip(),
        ]
        normalized_candidates = [
            item
            for item in dict.fromkeys(candidate for candidate in candidates if candidate)
            if item
        ]
        if not normalized_candidates:
            return None

        for candidate in normalized_candidates:
            employee = self.db.scalar(
                select(Employee)
                .where(
                    Employee.tenant_id == tenant_id,
                    or_(
                        Employee.id == candidate,
                        func.lower(Employee.email) == candidate.lower(),
                        func.lower(Employee.employee_no) == candidate.lower(),
                    ),
                )
                .limit(1)
            )
            if employee is not None:
                return employee

        for candidate in normalized_candidates:
            matches = list(
                self.db.scalars(
                    select(Employee)
                    .where(
                        Employee.tenant_id == tenant_id,
                        Employee.name == candidate,
                    )
                    .limit(2)
                ).all()
            )
            if len(matches) == 1:
                return matches[0]
        return None

    def _resolve_origin_city(
        self,
        payload: TravelReimbursementCalculatorRequest,
        current_user: CurrentUserContext,
        policy: RuntimeTravelPolicy,
    ) -> str:
        origin_location = str(payload.origin_location or "").strip()
        if not origin_location:
            employee = self._resolve_current_employee(current_user)
            origin_location = str(employee.location or "").strip() if employee is not None else ""
        if not origin_location:
            origin_location = "武汉"
        return (
            self._resolve_city(origin_location, policy)
            or self._resolve_other_region(origin_location)
            or origin_location
        )

    @staticmethod
    def _normalize_transport_mode(value: str | None) -> str:
        normalized = re.sub(r"\s+", "", str(value or ""))
        if any(keyword in normalized for keyword in ("飞机", "机票", "航班", "乘机", "坐飞机")):
            return "飞机"
        if any(keyword in normalized for keyword in ("火车", "高铁", "动车", "铁路", "列车")):
            return "火车"
        if any(
            keyword in normalized for keyword in ("轮船", "船票", "客轮", "渡轮", "邮轮", "坐船")
        ):
            return "轮船"
        return normalized if normalized in {"飞机", "火车", "轮船"} else ""

    def _resolve_transport_estimate(
        self,
        policy: RuntimeTravelPolicy,
        *,
        origin_city: str,
        destination_city: str,
        destination_text: str,
        transport_mode: str,
    ) -> dict[str, object]:
        if self._normalize_city_key(origin_city) == self._normalize_city_key(destination_city):
            return {}

        location_band = self._resolve_transport_location_band(destination_city or destination_text)
        candidate_modes = [transport_mode] if transport_mode else ["火车", "飞机", "轮船"]
        matched = None
        matched_mode = ""
        for candidate_mode in candidate_modes:
            candidates: list[tuple[int, object]] = []
            for estimate in policy.transport_estimates:
                if str(estimate.transport_mode or "").strip() != candidate_mode:
                    continue
                origin_score = self._transport_origin_match_score(
                    str(estimate.origin_city or ""), origin_city
                )
                if origin_score <= 0:
                    continue
                destination_score = self._transport_destination_match_score(
                    str(estimate.destination_city or ""),
                    destination_city or destination_text,
                    str(estimate.location_band or ""),
                    location_band,
                )
                if destination_score <= 0:
                    continue
                candidates.append((origin_score + destination_score, estimate))
            if candidates:
                _, matched = sorted(candidates, key=lambda item: item[0], reverse=True)[0]
                matched_mode = candidate_mode
                break
        if matched is None:
            return {}
        amount = Decimal(matched.round_trip_amount or Decimal("0.00")).quantize(Decimal("0.01"))
        if amount <= Decimal("0.00"):
            return {}
        origin_label = str(matched.origin_city or "").strip()
        if origin_label in {"*", "默认", "通用"}:
            origin_label = origin_city
        destination_label = str(matched.destination_city or "").strip() or (
            destination_city or self._transport_location_band_label(location_band)
        )
        basis = str(matched.basis or "").strip()
        if not basis:
            basis = f"{origin_label}-{destination_label}{matched_mode}往返预估"
        return {
            "amount": amount,
            "origin_city": origin_label,
            "destination_city": destination_label,
            "transport_mode": matched_mode,
            "basis": basis,
            "confidence": str(matched.confidence or "basic_rule").strip(),
            "source": "basic_rule_transport_estimate",
        }

    @staticmethod
    def _normalize_city_key(value: str) -> str:
        return re.sub(r"(省|市|区|县|自治州|特别行政区)$", "", str(value or "").strip())

    def _transport_origin_match_score(self, configured_origin: str, origin_city: str) -> int:
        normalized = self._normalize_city_key(configured_origin)
        if not normalized or normalized in {"*", "默认", "通用"}:
            return 10
        origin_key = self._normalize_city_key(origin_city)
        if normalized == origin_key or normalized in origin_key or origin_key in normalized:
            return 30
        return 0

    def _transport_destination_match_score(
        self,
        configured_destination: str,
        destination_city: str,
        configured_band: str,
        location_band: str,
    ) -> int:
        destination_key = self._normalize_city_key(destination_city)
        configured_key = self._normalize_city_key(configured_destination)
        if configured_key and (
            configured_key == destination_key
            or configured_key in destination_key
            or destination_key in configured_key
        ):
            return 70
        if configured_key:
            return 0
        if configured_band and configured_band == location_band:
            return 40
        return 0

    @staticmethod
    def _resolve_transport_location_band(location: str) -> str:
        text = str(location or "").strip()
        if any(
            keyword in text
            for keyword in (
                "新疆",
                "西藏",
                "青海",
                "甘肃",
                "宁夏",
                "内蒙古",
                "海南",
                "三亚",
                "海口",
                "香港",
                "澳门",
                "台湾",
                "海外",
                "国外",
            )
        ):
            return "remote"
        if any(
            keyword in text
            for keyword in (
                "北京",
                "上海",
                "广州",
                "深圳",
                "杭州",
                "南京",
                "苏州",
                "成都",
                "重庆",
                "天津",
            )
        ):
            return "premium"
        if any(keyword in text for keyword in ("厦门", "福州", "青岛", "大连", "宁波", "舟山")):
            return "coastal"
        return "default"

    @staticmethod
    def _transport_location_band_label(location_band: str) -> str:
        return {
            "premium": "高频城市",
            "remote": "远途地区",
            "coastal": "沿海城市",
            "default": "普通城市",
        }.get(str(location_band or "").strip(), "普通城市")

    @staticmethod
    def _resolve_city(location: str, policy: RuntimeTravelPolicy) -> str:
        normalized = str(location or "").strip()
        if not normalized:
            return ""
        city_names = set(policy.city_tiers.keys())
        city_names.update(policy.hotel_city_limits.keys())
        for city in sorted(city_names, key=lambda item: len(item), reverse=True):
            if (
                city in AMBIGUOUS_PROVINCE_CITY_NAMES
                and normalized != city
                and f"{city}市" not in normalized
            ):
                continue
            if city and city in normalized:
                return city
        compact = re.sub(r"(省|市|区|县|自治州|特别行政区)$", "", normalized)
        for city in sorted(city_names, key=lambda item: len(item), reverse=True):
            if (
                city in AMBIGUOUS_PROVINCE_CITY_NAMES
                and compact != city
                and f"{city}市" not in normalized
            ):
                continue
            if city and city in compact:
                return city
        return ""

    @staticmethod
    def _resolve_hotel_rate(
        policy: RuntimeTravelPolicy,
        grade_band: str,
        matched_city: str,
        city_tier: str,
        travel_date: date | None = None,
    ) -> Decimal:
        city_limits = policy.hotel_city_limits.get(matched_city, {}) if matched_city else {}
        base_rate = Decimal("0")
        for candidate in travel_policy_grade_key_candidates(grade_band):
            if city_limits.get(candidate) is not None:
                base_rate = Decimal(city_limits[candidate])
                break

        if base_rate <= Decimal("0"):
            for candidate in travel_policy_grade_key_candidates(grade_band):
                band_limits = policy.hotel_limits.get(candidate, {})
                if band_limits.get(city_tier) is not None:
                    base_rate = Decimal(band_limits[city_tier])
                    break
                if band_limits.get("tier_3") is not None:
                    base_rate = Decimal(band_limits["tier_3"])
                    break
        peak_rate = TravelReimbursementCalculatorService._resolve_peak_hotel_rate(
            policy,
            matched_city,
            travel_date,
        )
        return max(base_rate, peak_rate)

    @staticmethod
    def _resolve_peak_hotel_rate(
        policy: RuntimeTravelPolicy,
        matched_city: str,
        travel_date: date | None,
    ) -> Decimal:
        if not matched_city or travel_date is None:
            return Decimal("0")
        period = (getattr(policy, "hotel_peak_periods", {}) or {}).get(matched_city, "")
        if not period or not TravelReimbursementCalculatorService._month_in_peak_period(
            travel_date.month,
            period,
        ):
            return Decimal("0")
        peak_rate = (getattr(policy, "hotel_peak_city_limits", {}) or {}).get(matched_city)
        return Decimal(peak_rate or Decimal("0"))

    @staticmethod
    def _month_in_peak_period(month: int, period: str) -> bool:
        for part in re.split(r"[,，、;；]+", str(period or "")):
            if not part:
                continue
            if "-" not in part:
                try:
                    if int(part) == month:
                        return True
                except ValueError:
                    continue
                continue
            start_text, end_text = part.split("-", 1)
            try:
                start, end = int(start_text), int(end_text)
            except ValueError:
                continue
            if start <= end and start <= month <= end:
                return True
            if start > end and (month >= start or month <= end):
                return True
        return False

    @staticmethod
    def _resolve_allowance_region(location: str, matched_city: str) -> str:
        text = f"{location} {matched_city}".strip()
        if any(keyword in text for keyword in ("国外", "境外", "海外")):
            return "国外"
        if any(keyword in text for keyword in ("香港", "澳门", "台湾", "港澳台")):
            return "港澳台"
        if "乌鲁木齐" in text:
            return "新疆-乌鲁木齐"
        if "新疆" in text:
            return "新疆-其他"
        if "西藏" in text or "拉萨" in text:
            return "西藏"
        if any(
            keyword in text
            for keyword in ("北京", "上海", "天津", "重庆", "深圳", "珠海", "汕头", "厦门")
        ):
            return "直辖市/特区"
        return "其他地区"

    @staticmethod
    def _resolve_allowance_rate(
        policy: RuntimeTravelPolicy,
        allowance_key: str,
        region: str,
    ) -> Decimal:
        limits = policy.allowance_limits.get(allowance_key, {})
        if limits.get(region) is not None:
            return Decimal(limits[region])
        if limits.get("其他地区") is not None:
            return Decimal(limits["其他地区"])
        return Decimal("0")

    def _resolve_total_allowance_rate(
        self,
        policy: RuntimeTravelPolicy,
        region: str,
        meal_rate: Decimal,
        basic_rate: Decimal,
    ) -> Decimal:
        total_limits = policy.allowance_limits.get("total", {})
        if total_limits.get(region) is not None:
            return Decimal(total_limits[region])
        if total_limits.get("其他地区") is not None:
            return Decimal(total_limits["其他地区"])
        return meal_rate + basic_rate

    @staticmethod
    def _format_money(value: Decimal | int | float | str) -> str:
        return f"{Decimal(str(value)).quantize(Decimal('0.01'))}"
