import csv
import io
from datetime import UTC, datetime
from typing import AsyncIterator, override

from httpx import Response
from loguru import logger

from bb_integrations_lib.gravitate.rita_api import GravitateRitaAPI
from bb_integrations_lib.gravitate.sd_api import GravitateSDAPI
from bb_integrations_lib.mappers.rita_mapper import (
    FastMapDirection,
    RitaAPIMappingProvider,
    RitaMapper,
)
from bb_integrations_lib.models.fuelquest import FuelQuestConfig
from bb_integrations_lib.models.fuelquest.delivery_plan_converter import (
    FuelQuestDeliveryPlanConverter,
)
from bb_integrations_lib.models.fuelquest.shared import (
    Carrier,
    CountryCode,
    OrganizationId,
    TruckTerminal,
)
from bb_integrations_lib.models.rita.issue import IssueBase
from bb_integrations_lib.models.rita.mapping import MappingType
from bb_integrations_lib.protocols.pipelines import Step
from bb_integrations_lib.shared.model import GetFreightInvoicesRequest, RawData


class FuelQuestExportDeliveryPlanStep(Step):
    @override
    def __init__(
        self,
        rita_client: GravitateRitaAPI,
        sd_client: GravitateSDAPI,
        config: FuelQuestConfig,
        config_id: str,
        *args,
        **kwargs,
    ):
        self.rita_client = rita_client
        self.sd_client = sd_client
        self.config = config
        self.config_id = config_id

        self.self_carrier = Carrier(
            name=self.config.carrier_name,
            state=self.config.carrier_home_state,
            country_code=CountryCode(code="US"),
            organization_id=OrganizationId(ident=self.config.carrier_ident),
            # Will be replaced with the relevant terminal for each DeliveryConfirmation
            truck_terminal=TruckTerminal(
                name="Terminal", organization_id=OrganizationId(ident="12345")
            ),
            email=", ".join(
                self.config.carrier_emails  # Integration alerts will be sent here
            ),
        )

        self.rita_mapper = RitaMapper(
            RitaAPIMappingProvider(rita_client), config.source_system
        )

        super().__init__(*args, **kwargs)

    def describe(self) -> str:
        return "Convert Gravitate orders to FuelQuest XML"

    def issues_to_file(self, issues: list[IssueBase]) -> str:
        rows = [i.model_dump(include={"problem_short", "problem_long"}) for i in issues]
        out = io.StringIO()
        writer = csv.DictWriter(out, fieldnames=("problem_short", "problem_long"))
        writer.writeheader()
        writer.writerows(rows)
        out.seek(0)
        return out.read()

    async def generator(self, i: list[dict]) -> AsyncIterator[RawData]:
        if not i:
            logger.info("Nothing to export, stopping")
            return

        await self.rita_mapper.load_mappings_async()

        # We're using hyphens as a temporary patch to disambiguate otherwise identical source IDs.
        # Only use the portion of the map source id with the hyphen before it.
        for key, maps in self.rita_mapper.fast_maps.items():
            for m in maps:
                # Don't apply for source->Gravi maps, since this step doesn't use those
                if key.direction == FastMapDirection.grav_to_source:
                    # Only apply to the FQ ID maps
                    if key.type == MappingType.composite and (fqid := m.source_id.key.get("fuelquest id")) is not None:
                        new_id = fqid.split("-", 1)[0]
                        m.source_id.key["fuelquest id"] = new_id
                    elif key.type == MappingType.counterparty:
                        m.source_id = m.source_id.split("-", 1)[0]

        invoices_resp: Response = await self.sd_client.get_freight_invoices(
            req=GetFreightInvoicesRequest(
                # exported=True,
                # status="open",
                book_type="Revenue",
                order_numbers=[o["order_number"] for o in i],
            )
        )
        invoices_resp.raise_for_status()
        invoices = invoices_resp.json()
        if not invoices:
            logger.warning(
                f"No invoices found for any of these order numbers: {[o['order_number'] for o in i]}"
            )

        file_timestamp = datetime.now(UTC)

        converter = FuelQuestDeliveryPlanConverter(
            rita_mapper=self.rita_mapper,
            sd_client=self.sd_client,
            transmission_datetime=file_timestamp,
            self_carrier=self.self_carrier,
            transmission_sender=self.config.sender_fq_id,
            transmission_receiver=self.config.receiver_fq_id,
            allowed_counterparties=self.config.enabled_counterparties,
            allowed_store_markets=self.config.enabled_store_markets,
        )
        await converter.load_ref_data()
        count = 0
        async for xml_model in converter.convert(order_list=i, invoices=invoices):
            xml_str = xml_model.to_xml(pretty_print=True, xml_declaration=True)

            file_name = (
                f"DeliveryPlan_{file_timestamp.strftime('%Y%m%d%H%M%S')}_{count}.xml"
            )
            self.pipeline_context.included_files[file_name] = xml_str

            yield RawData(data=xml_str, file_name=file_name)
            count += 1
        if len(converter.issues) > 0:
            logger.warning(f"Got {len(converter.issues)} issues while converting")
            for issue in converter.issues:
                issue.config_id = self.config_id
            self.pipeline_context.issues.extend(converter.issues)
            self.pipeline_context.included_files["converter_issues.csv"] = (
                self.issues_to_file(converter.issues)
            )

        skipped = converter.skipped_orders_filter
        if len(skipped) > 0:
            logger.info(f"Skipped {len(skipped)} orders due to configured filters: {skipped}")

        total_unskipped = len(i) - len(skipped)
        if total_unskipped <= 0:
            logger.info("No invoices generated")
        else:
            logger.info(
                f"Generated {count} FuelQuest DeliveryPlans (success rate: {count / total_unskipped:.2%})"
            )
            self.pipeline_context.included_files["exported_orders.csv"] = "\n".join(
                sorted(converter.successful_order_numbers)
            )
