"""Flow Compiler - FlowPlan to Salesforce Flow XML.

Key corrections from Salesforce best practices:
- Use <recordUpdates> instead of <assignments> for $Record updates
- filterLogic: lowercase "and"/"or" only (no numeric strings in v1)
- Include doesRequireRecordChangedToMeetCriteria=true for Update triggers
- IsNull/IsNotNull: emit <booleanValue>true/false</booleanValue>
- Deploy zip: flows/<name>.flow (MDAPI format)
"""

from __future__ import annotations

import re
from xml.dom import minidom
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.sax.saxutils import escape as xml_escape

from ..api.v2.flow_builder_schemas import (
    ConditionOperator,
    FlowPlan,
    ScoutContext,
    TriggerEvent,
)

FLOW_NAMESPACE = "http://soap.sforce.com/2006/04/metadata"
API_VERSION = "60.0"


class FlowCompiler:
    def compile(self, plan: FlowPlan, context: ScoutContext) -> str:
        """Compile FlowPlan to Flow XML string.

        Note: XML special characters (<, >, &, ", ') in string values are
        automatically escaped by ElementTree when calling tostring().
        """
        flow = Element("Flow")
        flow.set("xmlns", FLOW_NAMESPACE)

        SubElement(flow, "apiVersion").text = API_VERSION
        SubElement(flow, "description").text = plan.description
        SubElement(flow, "label").text = (
            plan.flow_name.replace("FO_", "").replace("_", " ")
        )
        SubElement(flow, "processType").text = "AutoLaunchedFlow"

        # Status is "Draft" for inactive flows
        SubElement(flow, "status").text = "Draft"

        SubElement(flow, "triggerOrder").text = "1"

        target = "Update_Record"
        if plan.branches:
            target = "Branch_Decision"

        # Start element with entry criteria
        start = self._build_start(plan, target)
        flow.append(start)

        if plan.branches:
            default_end_name = self._safe_element_name("Branch_Default_End")
            decision = self._build_decision(plan, default_end_name)
            flow.append(decision)
            for idx, branch in enumerate(plan.branches, start=1):
                name = self._safe_element_name(f"Update_{idx}_{branch.name}")
                record_update = self._build_record_updates(
                    actions=branch.actions,
                    name=name,
                    label=branch.name,
                    location_y=300 + (idx - 1) * 120,
                )
                flow.append(record_update)
            flow.append(
                self._build_end(
                    name=default_end_name,
                    label="Default Outcome",
                    location_y=300 + len(plan.branches) * 120,
                )
            )
        else:
            # recordUpdates element (canonical pattern for $Record updates)
            record_update = self._build_record_updates(
                actions=plan.actions,
                name="Update_Record",
                label="Set Fields",
                location_y=311,
            )
            flow.append(record_update)

        xml_str = tostring(flow, encoding="unicode")
        return minidom.parseString(xml_str).toprettyxml(indent="    ")

    def _build_start(self, plan: FlowPlan, target_reference: str) -> Element:
        start = Element("start")
        connector = SubElement(start, "connector")
        SubElement(connector, "targetReference").text = target_reference
        SubElement(start, "locationX").text = "176"
        SubElement(start, "locationY").text = "0"
        SubElement(start, "object").text = plan.trigger.object_api_name

        events_set = set(plan.trigger.events)
        has_update = TriggerEvent.UPDATE in events_set

        if events_set == {TriggerEvent.CREATE}:
            SubElement(start, "recordTriggerType").text = "Create"
        elif events_set == {TriggerEvent.UPDATE}:
            SubElement(start, "recordTriggerType").text = "Update"
        else:
            SubElement(start, "recordTriggerType").text = "CreateAndUpdate"

        SubElement(start, "triggerType").text = plan.trigger.type.value

        if has_update and plan.entry_criteria and plan.entry_criteria.has_conditions():
            SubElement(start, "doesRequireRecordChangedToMeetCriteria").text = "true"

        if plan.entry_criteria and plan.entry_criteria.has_conditions():
            self._add_filters(start, plan.entry_criteria)

        return start

    def _build_decision(self, plan: FlowPlan, default_target: str) -> Element:
        decision = Element("decisions")
        SubElement(decision, "name").text = "Branch_Decision"
        SubElement(decision, "label").text = "Branch Decision"
        SubElement(decision, "locationX").text = "176"
        SubElement(decision, "locationY").text = "140"
        SubElement(decision, "defaultConnectorLabel").text = "Default Outcome"
        default_connector = SubElement(decision, "defaultConnector")
        SubElement(default_connector, "targetReference").text = default_target

        for idx, branch in enumerate(plan.branches, start=1):
            rule = SubElement(decision, "rules")
            rule_name = self._safe_element_name(f"Branch_{idx}_{branch.name}")
            SubElement(rule, "name").text = rule_name
            SubElement(rule, "label").text = branch.name
            SubElement(rule, "conditionLogic").text = (
                "and" if branch.logic == "AND" else "or"
            )
            self._add_decision_conditions(rule, branch)

            connector = SubElement(rule, "connector")
            SubElement(connector, "targetReference").text = self._safe_element_name(
                f"Update_{idx}_{branch.name}"
            )

        return decision

    def _build_end(self, *, name: str, label: str, location_y: int) -> Element:
        end = Element("end")
        SubElement(end, "name").text = name
        SubElement(end, "label").text = label
        SubElement(end, "locationX").text = "176"
        SubElement(end, "locationY").text = str(location_y)
        return end

    def _add_filters(self, start: Element, criteria) -> None:
        """Add filter logic and conditions to start element."""
        logic_str = "and" if criteria.logic == "AND" else "or"
        SubElement(start, "filterLogic").text = logic_str

        for cond in criteria.conditions:
            f = SubElement(start, "filters")
            SubElement(f, "field").text = cond.field

            if cond.operator == ConditionOperator.IS_NULL:
                SubElement(f, "operator").text = "IsNull"
                value_elem = SubElement(f, "value")
                SubElement(value_elem, "booleanValue").text = "true"
            elif cond.operator == ConditionOperator.IS_NOT_NULL:
                SubElement(f, "operator").text = "IsNull"
                value_elem = SubElement(f, "value")
                SubElement(value_elem, "booleanValue").text = "false"
            else:
                SubElement(f, "operator").text = cond.operator.value
                if cond.value is not None:
                    value_elem = SubElement(f, "value")
                    if cond.value_is_field:
                        SubElement(value_elem, "elementReference").text = (
                            f"$Record.{cond.value}"
                        )
                    elif isinstance(cond.value, bool):
                        SubElement(value_elem, "booleanValue").text = str(
                            cond.value
                        ).lower()
                    elif isinstance(cond.value, (int, float)):
                        SubElement(value_elem, "numberValue").text = str(cond.value)
                    else:
                        SubElement(value_elem, "stringValue").text = str(cond.value)

    def _add_decision_conditions(self, rule: Element, branch) -> None:
        """Add conditions to decision rule."""
        for cond in branch.conditions:
            c = SubElement(rule, "conditions")
            SubElement(c, "leftValueReference").text = f"$Record.{cond.field}"

            if cond.operator == ConditionOperator.IS_NULL:
                SubElement(c, "operator").text = "IsNull"
                value_elem = SubElement(c, "rightValue")
                SubElement(value_elem, "booleanValue").text = "true"
            elif cond.operator == ConditionOperator.IS_NOT_NULL:
                SubElement(c, "operator").text = "IsNull"
                value_elem = SubElement(c, "rightValue")
                SubElement(value_elem, "booleanValue").text = "false"
            else:
                SubElement(c, "operator").text = cond.operator.value
                if cond.value is not None:
                    value_elem = SubElement(c, "rightValue")
                    if cond.value_is_field:
                        SubElement(value_elem, "elementReference").text = (
                            f"$Record.{cond.value}"
                        )
                    elif isinstance(cond.value, bool):
                        SubElement(value_elem, "booleanValue").text = str(
                            cond.value
                        ).lower()
                    elif isinstance(cond.value, (int, float)):
                        SubElement(value_elem, "numberValue").text = str(cond.value)
                    else:
                        SubElement(value_elem, "stringValue").text = str(cond.value)

    def _build_record_updates(
        self, *, actions, name: str, label: str, location_y: int
    ) -> Element:
        """
        Build recordUpdates element (canonical pattern for RecordBeforeSave).
        Uses inputReference=$Record and inputAssignments for field updates.
        """
        ru = Element("recordUpdates")
        SubElement(ru, "name").text = name
        SubElement(ru, "label").text = label
        SubElement(ru, "locationX").text = "176"
        SubElement(ru, "locationY").text = str(location_y)

        SubElement(ru, "inputReference").text = "$Record"

        for action in actions:
            ia = SubElement(ru, "inputAssignments")
            SubElement(ia, "field").text = action.field

            value_elem = SubElement(ia, "value")
            if action.value.source == "field_reference":
                SubElement(value_elem, "elementReference").text = (
                    f"$Record.{action.value.content}"
                )
            elif action.value.source == "formula":
                SubElement(value_elem, "formula").text = str(action.value.content or "")
            elif isinstance(action.value.content, bool):
                SubElement(value_elem, "booleanValue").text = str(
                    action.value.content
                ).lower()
            elif isinstance(action.value.content, (int, float)):
                SubElement(value_elem, "numberValue").text = str(action.value.content)
            else:
                SubElement(value_elem, "stringValue").text = str(
                    action.value.content or ""
                )

        return ru

    @staticmethod
    def _safe_element_name(text: str) -> str:
        name = re.sub(r"[^A-Za-z0-9_]", "_", text or "").strip("_")
        if not name or not name[0].isalpha():
            name = f"Elem_{name}" if name else "Elem"
        return name[:80]


def build_deploy_zip(flow_xml: str, flow_api_name: str) -> bytes:
    """
    Build Metadata API deploy zip.

    Structure:
        flows/<flow_api_name>.flow
        package.xml
    """
    import io
    import zipfile

    # Escape flow_api_name for XML safety (defense-in-depth, though names
    # are validated to be alphanumeric). ElementTree escapes automatically
    # for .text assignments, but f-string XML needs explicit escaping.
    safe_flow_name = xml_escape(flow_api_name)
    package_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <types>
        <members>{safe_flow_name}</members>
        <name>Flow</name>
    </types>
    <version>{API_VERSION}</version>
</Package>"""

    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
        zf.writestr(f"flows/{flow_api_name}.flow", flow_xml)
        zf.writestr("package.xml", package_xml)

    buffer.seek(0)
    return buffer.read()
