from __future__ import annotations

from datetime import date, datetime
import json
from pathlib import Path
from threading import RLock
from uuid import uuid4
from zoneinfo import ZoneInfo

from rdflib import Graph, Literal, Namespace, RDF, URIRef, XSD

from app.market.filters import rejection_reasons
from app.market.models import CandidateRecord, InquiryRequest
from app.market.pricing import build_prices
from app.market.scoring import CustomerSignals, score_candidates
from app.repository import RDFRepository


GESLI = Namespace("https://gesli.example/ontology#")
RUN = Namespace("https://gesli.example/recommendation/")


class MarketService:
    def __init__(self, root: Path, repository: RDFRepository):
        self.root = root
        self.repository = repository
        self.config = json.loads(
            (root / "config" / "market-strategies.json").read_text(encoding="utf-8")
        )
        self.runs: dict[str, dict] = {}
        self.audit_graph = Graph()
        self._lock = RLock()

    def customers(self) -> list[dict]:
        rows = self.repository.query_file(
            self.root / "queries" / "market-customers.rq"
        )["rows"]
        return [
            {
                "id": row["customer"],
                "name": row["customerName"],
                "industry": row.get("industry", ""),
                "preferred_route": row.get("routeName", ""),
                "repeat_purchase_count": _as_int(row.get("repeatPurchaseCount")),
                "price_sensitivity": _as_float(row.get("customerPriceSensitivity"), 0.5),
                "time_sensitivity": _as_float(row.get("customerTimeSensitivity"), 0.5),
                "reliability_sensitivity": _as_float(
                    row.get("customerReliabilitySensitivity"), 0.5
                ),
            }
            for row in rows
        ]

    def form_options(self) -> dict:
        candidates = self._candidate_records()
        context_rows = self.repository.query_file(
            self.root / "queries" / "market-context.rq"
        )["rows"]
        configured_routes = {
            (row.get("origin", ""), row.get("destination", ""))
            for row in context_rows
            if row.get("kind") == "benchmark" and row.get("origin") and row.get("destination")
        }
        origins = sorted({origin for origin, _ in configured_routes})
        destinations = sorted({destination for _, destination in configured_routes})
        route_types = sorted({
            item.route_type
            for item in candidates
            if item.route_type and (item.origin, item.destination) in configured_routes
        })
        benchmark_by_route = {
            (row.get("origin", ""), row.get("destination", "")): _as_float(
                row.get("amount")
            )
            for row in context_rows
            if row.get("kind") == "benchmark"
        }
        routes = []
        for origin, destination in sorted(configured_routes):
            route_candidates = [
                item
                for item in candidates
                if item.origin == origin
                and item.destination == destination
                and item.departure_date
                and item.valid_to
                and item.valid_to >= date(2026, 7, 20)
            ]
            departures = [item.departure_date for item in route_candidates]
            transit_days = [item.transit_days for item in route_candidates]
            routes.append({
                "origin": origin,
                "destination": destination,
                "route_types": sorted({
                    item.route_type for item in route_candidates if item.route_type
                }),
                "target_amount": benchmark_by_route.get((origin, destination), 0),
                "requested_departure_date": (
                    min(departures).isoformat() if departures else "2026-07-27"
                ),
                "max_transit_days": max(transit_days) if transit_days else 30,
            })
        return {
            "customers": self.customers(),
            "origins": origins,
            "destinations": destinations,
            "route_types": route_types,
            "routes": routes,
            "defaults": InquiryRequest(customer="https://gesli.example/sample#Haier").model_dump(
                mode="json"
            ),
        }

    def recommend(self, inquiry: InquiryRequest) -> dict:
        customer_row = self._find_customer(inquiry.customer)
        customer = self._customer_signals(customer_row)
        component_cost, component_names = self._component_costs(
            inquiry.evaluation_date, inquiry.origin
        )
        component_product_codes = {
            code
            for codes in self.config["component_product_codes_by_origin"].values()
            for code in codes
        }

        route_candidates = [
            candidate
            for candidate in self._candidate_records()
            if candidate.origin == inquiry.origin
            and candidate.destination == inquiry.destination
            and candidate.currency == "CNY"
            and candidate.product_code not in component_product_codes
        ]

        feasible: list[CandidateRecord] = []
        rejected: list[dict] = []
        for candidate in route_candidates:
            reasons = rejection_reasons(candidate, inquiry)
            if reasons:
                rejected.append(self._rejected_payload(candidate, reasons))
            else:
                feasible.append(candidate)

        if not feasible:
            raise ValueError("没有满足当前硬条件的供应商产品和班期")

        benchmark, strategy = self._market_context(inquiry)
        scored, weights = score_candidates(feasible, inquiry, customer)

        candidates: list[dict] = []
        for item in scored:
            candidate: CandidateRecord = item.pop("candidate")
            total_supplier_cost = candidate.supplier_cost + component_cost
            prices = build_prices(
                benchmark_amount=benchmark["amount"],
                strategy_rate=strategy["adjustment_rate"],
                solution_type=item["solution_type"],
                customer=customer,
                salesperson_adjustment=inquiry.salesperson_adjustment,
                supplier_cost=total_supplier_cost,
                config=self.config,
            )
            payload = self._candidate_payload(candidate, item, prices, component_names)
            candidates.append(payload)

        top = candidates[0]
        score_gap = top["total_score"] - candidates[1]["total_score"] if len(candidates) > 1 else 10
        confidence = round(min(0.98, 0.78 + max(0, score_gap) / 100), 2)
        top["confidence_score"] = confidence

        run_id = f"rec-{uuid4().hex[:12]}"
        generated_at = datetime.now(ZoneInfo("Asia/Shanghai")).isoformat(timespec="seconds")
        result = {
            "run_id": run_id,
            "generated_at": generated_at,
            "normalized_inquiry": {
                **inquiry.model_dump(mode="json"),
                "customer_name": customer_row["customerName"],
                "route": f"{inquiry.origin} → {inquiry.destination}",
                "dangerous_goods_label": "危险品" if inquiry.dangerous_goods else "非危险品",
            },
            "pipeline": {
                "discovered": len(route_candidates),
                "rejected": len(rejected),
                "feasible": len(feasible),
                "generated_solutions": len(candidates),
            },
            "recommendation": {
                "candidate_id": top["id"],
                "solution_type": top["solution_type"],
                "product_name": top["product_name"],
                "supplier_name": top["supplier_name"],
                "final_quote_amount": top["pricing"]["final_quote_amount"],
                "currency": top["pricing"]["currency"],
                "confidence_score": confidence,
                "summary": self._recommendation_summary(top, strategy, customer_row),
            },
            "market_context": {
                "benchmark_amount": benchmark["amount"],
                "currency": benchmark["currency"],
                "strategy_name": strategy["name"],
                "strategy_objective": strategy["objective"],
                "strategy_adjustment_rate": strategy["adjustment_rate"],
            },
            "weights": weights,
            "candidates": candidates,
            "rejected_candidates": rejected,
        }
        self._store_run(run_id, result, inquiry, strategy)
        return result

    def get_run(self, run_id: str) -> dict | None:
        with self._lock:
            return self.runs.get(run_id)

    def reset_runtime(self) -> None:
        with self._lock:
            self.runs.clear()
            self.audit_graph = Graph()

    def get_audit(self, run_id: str) -> dict | None:
        run = self.get_run(run_id)
        if not run:
            return None
        run_uri = RUN[run_id]
        with self._lock:
            triples = list(self.audit_graph.triples((run_uri, None, None)))
        return {
            "run_id": run_id,
            "generated_at": run["generated_at"],
            "input": run["normalized_inquiry"],
            "weights": run["weights"],
            "market_context": run["market_context"],
            "candidate_count": len(run["candidates"]),
            "rejected_count": len(run["rejected_candidates"]),
            "rdf_assertions": len(triples),
            "sources": self.repository.loaded_file_names,
        }

    def _candidate_records(self) -> list[CandidateRecord]:
        rows = self.repository.query_file(
            self.root / "queries" / "market-candidates.rq"
        )["rows"]
        return [
            CandidateRecord(
                quote_uri=row["quote"],
                quote_no=row["quoteNo"],
                product_uri=row["product"],
                product_code=row.get("productCode", ""),
                product_name=row.get("productName", ""),
                brand_name=row.get("brandName", ""),
                transport_mode=row.get("transportMode", ""),
                offering_uri=row["offering"],
                offering_code=row.get("offeringCode", ""),
                supplier_uri=row["supplier"],
                supplier_name=row["supplierName"],
                schedule_uri=row.get("schedule", ""),
                schedule_code=row.get("scheduleCode", ""),
                route_type=row.get("routeType", ""),
                service_form=row.get("serviceForm", ""),
                origin=row.get("originLocation", ""),
                destination=row.get("destinationLocation", ""),
                currency=row["currency"],
                supplier_cost=_as_float(row.get("supplierCostAmount")),
                valid_from=_as_date(row.get("validFrom")),
                valid_to=_as_date(row.get("validTo")),
                departure_date=_as_date(row.get("estimatedDepartureTime")),
                arrival_date=_as_date(row.get("estimatedArrivalTime")),
                transit_days=_as_float(row.get("estimatedTransitDays")),
                booking_success_rate=_as_float(row.get("bookingSuccessRate"), 0.8),
                space_release_success_rate=_as_float(
                    row.get("spaceReleaseSuccessRate"), 0.8
                ),
                etd_on_time_rate=_as_float(row.get("etdAtdOnTimeRate"), 0.85),
                eta_on_time_rate=_as_float(row.get("etaAtaOnTimeRate"), 0.85),
                supplier_active=_as_bool(row.get("supplierActive"), True),
                capacity_available=_as_bool(row.get("capacityAvailable"), True),
                supports_dangerous_goods=_as_bool(
                    row.get("supportsDangerousGoods"), False
                ),
                dangerous_goods_only=_as_bool(row.get("dangerousGoodsOnly"), False),
                agency_level=row.get("agencyLevel", ""),
                offering_channel=row.get("offeringChannel", ""),
                capacity_commitment=row.get("capacityCommitment", ""),
            )
            for row in rows
        ]

    def _find_customer(self, customer_id: str) -> dict:
        rows = self.repository.query_file(
            self.root / "queries" / "market-customers.rq"
        )["rows"]
        for row in rows:
            if row["customer"] == customer_id or row["customerName"] == customer_id:
                return row
        raise ValueError("客户不存在或没有可用画像")

    @staticmethod
    def _customer_signals(row: dict) -> CustomerSignals:
        service_requirement = "；".join(
            value
            for value in (
                row.get("notificationRequirement", ""),
                row.get("documentRequirement", ""),
                row.get("exceptionResponseRequirement", ""),
            )
            if value
        )
        return CustomerSignals(
            price_sensitivity=_as_float(row.get("customerPriceSensitivity"), 0.5),
            time_sensitivity=_as_float(row.get("customerTimeSensitivity"), 0.5),
            reliability_sensitivity=_as_float(
                row.get("customerReliabilitySensitivity"), 0.5
            ),
            repeat_purchase_count=_as_int(row.get("repeatPurchaseCount")),
            overdue_days=_as_int(row.get("overdueDays")),
            service_requirement=service_requirement,
        )

    def _component_costs(
        self, evaluation_date: date, origin: str
    ) -> tuple[float, list[str]]:
        candidates = self._candidate_records()
        total = 0.0
        names: list[str] = []
        product_codes = self.config["component_product_codes_by_origin"].get(
            origin,
            self.config["component_product_codes_by_origin"].get("__default__", []),
        )
        for product_code in product_codes:
            valid = [
                item
                for item in candidates
                if item.product_code == product_code
                and (not item.valid_from or item.valid_from <= evaluation_date)
                and (not item.valid_to or evaluation_date <= item.valid_to)
            ]
            if valid:
                selected = min(valid, key=lambda item: item.supplier_cost)
                total += selected.supplier_cost
                names.append(selected.product_name)
        return total, names

    def _market_context(self, inquiry: InquiryRequest) -> tuple[dict, dict]:
        rows = self.repository.query_file(
            self.root / "queries" / "market-context.rq"
        )["rows"]
        applicable = [
            row
            for row in rows
            if row.get("origin") == inquiry.origin
            and row.get("destination") == inquiry.destination
            and _within(inquiry.evaluation_date, row.get("validFrom"), row.get("validTo"))
        ]
        benchmark_row = next((row for row in applicable if row["kind"] == "benchmark"), None)
        strategy_row = next((row for row in applicable if row["kind"] == "strategy"), None)
        if not benchmark_row or not strategy_row:
            raise ValueError("当前路线缺少有效的市场基准价格或启用策略")
        return (
            {
                "uri": benchmark_row["resource"],
                "amount": _as_float(benchmark_row["amount"]),
                "currency": benchmark_row["currency"],
            },
            {
                "uri": strategy_row["resource"],
                "name": strategy_row["strategyName"],
                "objective": strategy_row["strategyObjective"],
                "adjustment_rate": _as_float(strategy_row["strategyAdjustmentRate"]),
            },
        )

    @staticmethod
    def _candidate_payload(
        candidate: CandidateRecord,
        scores: dict,
        prices: dict,
        component_names: list[str],
    ) -> dict:
        reliability = round(
            (candidate.booking_success_rate + candidate.space_release_success_rate) / 2,
            2,
        )
        reasons = [
            f"供应商订舱与放舱综合成功率{reliability:.0%}",
            f"预计运输时间{candidate.transit_days:g}天",
            f"{candidate.route_type or '当前'}路由，{candidate.capacity_commitment or '运力已确认'}",
        ]
        if scores["solution_type"] == "经济方案":
            reasons.insert(0, "当前可执行候选中采购价格最有竞争力")
        elif scores["solution_type"] == "时效方案":
            reasons.insert(0, "当前可执行候选中预计运输时间最短")
        else:
            reasons.insert(0, "客户偏好、供应商可靠性与价格综合表现最均衡")

        return {
            "id": candidate.quote_uri,
            "quote_no": candidate.quote_no,
            "solution_type": scores["solution_type"],
            "rank": scores["rank"],
            "brand_name": candidate.brand_name,
            "product_name": candidate.product_name,
            "supplier_name": candidate.supplier_name,
            "offering_channel": candidate.offering_channel,
            "schedule_code": candidate.schedule_code,
            "departure_date": candidate.departure_date.isoformat() if candidate.departure_date else "",
            "arrival_date": candidate.arrival_date.isoformat() if candidate.arrival_date else "",
            "transit_days": candidate.transit_days,
            "route_type": candidate.route_type,
            "capacity_commitment": candidate.capacity_commitment,
            "components": [*component_names, candidate.product_name],
            "reliability_rate": reliability,
            "price_score": scores["price_score"],
            "transit_score": scores["transit_score"],
            "supplier_execution_score": scores["supplier_execution_score"],
            "customer_fit_score": scores["customer_fit_score"],
            "product_strategy_score": scores["product_strategy_score"],
            "total_score": scores["total_score"],
            "confidence_score": None,
            "reasons": reasons,
            "pricing": prices,
        }

    @staticmethod
    def _rejected_payload(candidate: CandidateRecord, reasons: list[str]) -> dict:
        return {
            "id": candidate.quote_uri,
            "quote_no": candidate.quote_no,
            "product_name": candidate.product_name,
            "supplier_name": candidate.supplier_name,
            "schedule_code": candidate.schedule_code,
            "reasons": reasons,
        }

    @staticmethod
    def _recommendation_summary(
        candidate: dict, strategy: dict, customer_row: dict
    ) -> str:
        return (
            f"为{customer_row['customerName']}推荐{candidate['solution_type']}："
            f"{candidate['supplier_name']}的{candidate['product_name']}。"
            f"综合得分{candidate['total_score']:.1f}，采用“{strategy['name']}”，"
            f"在时效、履约可靠性和客户价格接受度之间取得当前最优平衡。"
        )

    def _store_run(
        self, run_id: str, result: dict, inquiry: InquiryRequest, strategy: dict
    ) -> None:
        run_uri = RUN[run_id]
        generated_at = Literal(result["generated_at"], datatype=XSD.dateTime)
        with self._lock:
            self.runs[run_id] = result
            self.audit_graph.add((run_uri, RDF.type, GESLI.RecommendationRun))
            self.audit_graph.add((run_uri, GESLI.generatedAt, generated_at))
            self.audit_graph.add((run_uri, GESLI.usesMarketStrategy, URIRef(strategy["uri"])))
            self.audit_graph.add((run_uri, GESLI.recommendationReason, Literal(result["recommendation"]["summary"])))
            customer_uri = URIRef(inquiry.customer)
            self.audit_graph.add((run_uri, GESLI.quoteForCustomer, customer_uri))
            for candidate in result["candidates"]:
                evaluation_uri = RUN[f"{run_id}/candidate/{candidate['rank']}"]
                self.audit_graph.add((evaluation_uri, RDF.type, GESLI.CandidateEvaluation))
                self.audit_graph.add((run_uri, GESLI.hasCandidateEvaluation, evaluation_uri))
                self.audit_graph.add((evaluation_uri, GESLI.evaluatedQuote, URIRef(candidate["id"])))
                self.audit_graph.add((evaluation_uri, GESLI.recommendationRank, Literal(candidate["rank"])))
                self.audit_graph.add((evaluation_uri, GESLI.totalScore, Literal(candidate["total_score"])))
            top_evaluation = RUN[f"{run_id}/candidate/1"]
            self.audit_graph.add((run_uri, GESLI.recommendedEvaluation, top_evaluation))


def _as_float(value: str | None, default: float = 0.0) -> float:
    try:
        return float(value) if value not in (None, "") else default
    except (TypeError, ValueError):
        return default


def _as_int(value: str | None, default: int = 0) -> int:
    try:
        return int(value) if value not in (None, "") else default
    except (TypeError, ValueError):
        return default


def _as_bool(value: str | None, default: bool = False) -> bool:
    if value in (None, ""):
        return default
    return str(value).lower() in {"true", "1", "yes"}


def _as_date(value: str | None) -> date | None:
    if not value:
        return None
    try:
        return date.fromisoformat(value[:10])
    except ValueError:
        return None


def _within(current: date, start: str | None, end: str | None) -> bool:
    start_date = _as_date(start)
    end_date = _as_date(end)
    return (not start_date or start_date <= current) and (not end_date or current <= end_date)
