import json
from pathlib import Path
from .geometry import process_points, process_lines, process_polygons
from lxml import etree

class SigwxParser:
    """
    Parses WAFS IWXXM SIGWX XML files into a list of forecast objects.

    Because SIGWX data is grouped into specific 3-hour forecast blocks, this parser
    isolates each block, extracting its specific valid time and issue time. It then
    processes all meteorological features (icing, turbulence, jetstreams, etc.) within
    that block into a standard GeoJSON FeatureCollection.

    Returns a list of dictionaries structured like this:

        [
            {
                "valid_time": "2024-02-22T12:00:00Z",
                "issue_time": "2024-02-21T18:00:00Z",
                "feature_collection": {
                    "type": "FeatureCollection",
                    "features": [
                        {
                            "type": "Feature",
                            "geometry": {
                                "type": "Polygon",
                                "coordinates": [ [ [lon, lat], ... ] ]
                            },
                            "properties": {
                                "feature_id": "uuid.12345...",
                                "phenomenon": "AIRFRAME_ICING",
                                "upper_fl": "240",
                                "lower_fl": "100"
                            }
                        }
                    ]
                }
            }
        ]
    """
    def __init__(self, file_path: str, xml_content: str = None):
        """
        Initializes the parser with either a file path or raw XML content.

        Args:
            file_path (str): The path to the SIGWX XML file. Can be None if xml_content is provided.
            xml_content (str, optional): Raw XML string content. Defaults to None.

        Raises:
            FileNotFoundError: If the provided file_path does not exist.
            ValueError: If neither a valid file_path nor xml_content is provided.
        """
        self.file_path = file_path
        self.xml_content = xml_content
        self.root = None

        if file_path:
            path = Path(file_path)
            if not path.is_file():
                raise FileNotFoundError(f"Could not find a file at: {path.absolute()}")
            raw_text = path.read_text(encoding='utf-8')
        elif xml_content:
            raw_text = xml_content
        else:
            raise ValueError("Either file_path or xml_content must be provided.")
        self.root = self._clean_xml_string(raw_text)

    @staticmethod
    def _clean_xml_string(raw_text: str):
        """
        Strips namespaces, BOM characters, and whitespace from the raw XML text.

        Args:
            raw_text (str): The raw XML string.

        Returns:
            lxml.etree._Element: The cleaned root element of the XML tree.
        """
        clean_text = raw_text.replace('\uFEFF', '').strip()
        parser = etree.XMLParser(recover=True)
        root = etree.fromstring(clean_text.encode('utf-8'), parser=parser)

        for elem in root.iter():
            elem.tag = etree.QName(elem).localname

            clean_attribs = {etree.QName(k).localname: v for k, v in elem.attrib.items()}
            elem.attrib.clear()
            elem.attrib.update(clean_attribs)

        etree.cleanup_namespaces(root)

        return root

    @staticmethod
    def _safe_text(element, default="N/A"):
        """
        Safely extracts and strips text from an lxml element.

        Args:
            element (lxml.etree._Element): The XML element to extract text from.
            default (str, optional): The value to return if the element or its text is None. Defaults to "N/A".

        Returns:
            str: The extracted text or the default value.
        """
        if element is not None and element.text is not None:
            return element.text.strip()
        return default

    def parse(self, exclude: set = {}):
        """
        Executes the main parsing loop over the cleaned XML tree.

        Iterates through each WAFSSignificantWeatherForecast block, extracts
        the valid and issue times, and processes all associated meteorological
        features (polygons, points, lines) into a GeoJSON FeatureCollection.

        Args:
            exclude (set, optional): A set of feature type strings to skip
                during parsing. Features whose phenomenon type matches any
                value in this set will be excluded from the output.
                Defaults to None (no features excluded).

                Valid values are:
                    - ``AIRFRAME_ICING``
                    - ``CLOUD``
                    - ``JETSTREAM``
                    - ``TROPOPAUSE``
                    - ``TURBULENCE``
                    - ``RADIATION``
                    - ``TROPICAL_CYCLONE``
                    - ``VOLCANO``

        Returns:
            list: A list of forecast dictionaries containing valid times,
                issue times, and their corresponding GeoJSON FeatureCollections.
        """
        forecasts = self.root.xpath("//WAFSSignificantWeatherForecast")
        collections = []
        for f in forecasts:
            valid_time = self._safe_text(f.find(".//beginPosition"))
            issue_time = self._safe_text(f.find(".//timePosition"))
            feature_collection = {
                "type": "FeatureCollection",
                "features": []
            }
            met_features = f.xpath(".//MeteorologicalFeature")

            for feature in met_features:
                feature_type = feature.find(".//phenomenon").get("href").strip().split("/")[-1] or "N/A"
                if feature_type in exclude:
                    continue

                feature_id = feature.get("id").strip() if feature.get("id") else "N/A"
                upper_fl = self._safe_text(feature.find(".//upperElevation"))
                lower_fl = self._safe_text(feature.find(".//lowerElevation"))


                if feature.find(".//PolygonPatch") is not None:
                    processed_polygon = process_polygons(feature_type=feature_type,
                                                         polygon_patch=feature.find(".//PolygonPatch"),
                                                         feature_id=feature_id,
                                                         upper_fl=upper_fl,
                                                         lower_fl=lower_fl)

                    feature_collection.get("features").append(processed_polygon)

                elif feature.find(".//Point") is not None:
                    processed_point = process_points(feature_type=feature_type,
                                                     point=feature.find(".//Point"),
                                                     feature_id=feature_id,
                                                     upper_fl=upper_fl,
                                                     lower_fl=lower_fl)
                    feature_collection.get("features").append(processed_point)
                else:
                    processed_line = process_lines(feature_type=feature_type,
                                                   line=feature.find(".//CubicSpline"),
                                                   feature_id=feature_id,
                                                   upper_fl=upper_fl,
                                                   lower_fl=lower_fl)
                    feature_collection.get("features").append(processed_line)

            collections.append({
                "valid_time": valid_time,
                "issue_time": issue_time,
                "geojson": feature_collection})
        return json.dumps(collections)