import json
import zipfile
import logging
import io
import re

logger = logging.getLogger(__name__)


def apply_color_percent(hex_color, percent):
    """
    Applies Power BI style tint/shade percentage.
    percent > 0: Lighten (Blend with White)
    percent < 0: Darken (Blend with Black)
    """
    try:
        hex_color = hex_color.lstrip("#")
        if len(hex_color) == 6:
            r, g, b = tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4))
        else:
            return f"#{hex_color}" if not hex_color.startswith("#") else hex_color

        if percent == 0:
            return f"#{hex_color}"

        if percent > 0:
            r = int(r + (255 - r) * percent)
            g = int(g + (255 - g) * percent)
            b = int(b + (255 - b) * percent)
        else:
            factor = 1 + percent
            r = int(r * factor)
            g = int(g * factor)
            b = int(b * factor)

        return f"#{r:02X}{g:02X}{b:02X}"
    except Exception:
        return f"#{hex_color}" if not hex_color.startswith("#") else hex_color


def resolve_color(expr, theme=None):
    """
    Resolves a Power BI color expression to a hex string.
    Handles Literal and ThemeDataColor (with tint/shade).
    """
    if not expr:
        return None

    if "Literal" in expr:
        value = expr["Literal"].get("Value", "")
        if value.startswith("'") and value.endswith("'"):
            value = value[1:-1]
        return {"hex": value, "source": "Literal", "raw": expr}

    if "ThemeDataColor" in expr:
        tdc = expr["ThemeDataColor"]
        color_id = tdc.get("ColorId", 0)
        percent = tdc.get("Percent", 0)

        default_colors = theme.get("dataColors", []) if theme and "dataColors" in theme else [
            "#118DFF", "#12239E", "#E66C37", "#6B007B", "#E64483",
            "#7044E6", "#00A1C0", "#006E00", "#D3B700", "#C43628",
            "#F0F0F0", "#5F6B6D",
        ]

        if 0 <= color_id < len(default_colors):
            base_hex = default_colors[color_id]
        else:
            base_hex = "#000000"

        final_hex = apply_color_percent(base_hex, percent)

        return {"hex": final_hex, "source": "ThemeDataColor", "raw": expr}

    return {"hex": None, "source": "Unknown", "raw": expr}


def parse_transparency(expr, default=0):
    if not expr or "Literal" not in expr:
        return default

    val = expr["Literal"].get("Value", default)
    if isinstance(val, str):
        val = val.replace("D", "").replace("'", "")

    try:
        return float(val)
    except Exception:
        return default


def walk_find(obj, *keys):
    """
    Traverses a nested dict/list to find a specific key path.
    Usage: walk_find(obj, "singleVisual", "objects", "title")
    Returns the value or None.
    """
    current = obj
    for key in keys:
        if isinstance(current, dict):
            current = current.get(key)
        elif isinstance(current, list) and isinstance(key, int):
            if 0 <= key < len(current):
                current = current[key]
            else:
                return None
        else:
            return None

        if current is None:
            return None

    return current


def get_table_id(table_name):
    return f"t:{table_name}"


def get_column_id(table_name, col_name):
    return f"c:{table_name}[{col_name}]"


def get_measure_id(table_name, measure_name):
    return f"m:{table_name}[{measure_name}]"


def get_visual_id(page_name, visual_name):
    return f"v:{page_name}::{visual_name}"


def get_query_id(query_name):
    return f"q:{query_name}"


def extract_dax_dependencies(expression):
    """
    Basic regex-based DAX dependency extractor.
    Finds references like 'Table'[Column] or [Measure].
    """
    if not expression:
        return []

    dependencies = []

    table_col_pattern = r"'([^']+)'\[([^\]]+)\]"
    for match in re.finditer(table_col_pattern, expression):
        dependencies.append({
            "table": match.group(1),
            "object": match.group(2),
            "type": "column_or_measure",
        })

    col_pattern = r"(?<!')\[([^\]]+)\]"
    for match in re.finditer(col_pattern, expression):
        dependencies.append({
            "object": match.group(1),
            "type": "measure_or_local_column",
        })

    return dependencies


def parse_m_script(script):
    """
    Parses Section1.m content to extract queries and steps.
    """
    if not script:
        return []

    queries = []

    query_pattern = r"shared\s+#?\"?(.+?)\"?\s*=\s*let(.*?)\s*in\s+(.+?);"

    for match in re.finditer(query_pattern, script, re.DOTALL):
        name = match.group(1)
        body = match.group(2)
        final_result = match.group(3)

        steps = []
        lines = body.strip().split("\n")
        for line in lines:
            line = line.strip()
            if not line:
                continue

            step_match = re.match(r"^\s*#?\"?(.+?)\"?\s*=\s*(.*)", line)
            if step_match:
                step_name = step_match.group(1)
                expression = step_match.group(2)
                if expression.endswith(","):
                    expression = expression[:-1]

                steps.append({
                    "name": step_name,
                    "expression": expression.strip(),
                })

        queries.append({
            "name": name,
            "steps": steps,
            "final_result": final_result.strip(),
            "raw_m": f"let{body}in {final_result}",
        })

    return queries


def extract_query_metadata(prototype_query):
    """
    Extracts deep metadata from visual's prototypeQuery.
    Returns tables, columns, measures, where clauses, order by.
    """
    metadata = {
        "From": [],
        "Select": [],
        "Where": [],
        "OrderBy": [],
        "Raw": prototype_query,
    }

    if not prototype_query:
        return metadata

    def traverse(obj):
        if isinstance(obj, dict):
            if "Entity" in obj:
                if isinstance(obj["Entity"], str):
                    metadata["From"].append(obj["Entity"])

            if "Property" in obj:
                pass

            if "From" in obj and isinstance(obj["From"], list):
                for item in obj["From"]:
                    if "Entity" in item:
                        metadata["From"].append(item["Entity"])

            if "Select" in obj and isinstance(obj["Select"], list):
                for item in obj["Select"]:
                    col_info = {}

                    if "Column" in item:
                        col_expr = item["Column"]
                        if "Property" in col_expr:
                            col_info["name"] = col_expr["Property"]
                            col_info["type"] = "column"

                    elif "Measure" in item:
                        meas_expr = item["Measure"]
                        if "Property" in meas_expr:
                            col_info["name"] = meas_expr["Property"]
                            col_info["type"] = "measure"

                    elif "Aggregation" in item:
                        agg = item["Aggregation"]
                        if "Expression" in agg and "Column" in agg["Expression"]:
                            col_info["name"] = agg["Expression"]["Column"].get("Property")
                            col_info["type"] = "column_agg"
                            col_info["func"] = agg.get("Function")

                    elif "HierarchyLevel" in item:
                        hl = item["HierarchyLevel"]
                        if "Expression" in hl and "Hierarchy" in hl["Expression"]:
                            h_expr = hl["Expression"]["Hierarchy"]
                            if "Expression" in h_expr and "PropertyVariationSource" in h_expr["Expression"]:
                                pvs = h_expr["Expression"]["PropertyVariationSource"]
                                col_info["name"] = pvs.get("Property")
                                col_info["type"] = "column_hierarchy"
                                col_info["inferred_type"] = "dateTime"

                    if col_info:
                        metadata["Select"].append(col_info)

            for key, value in obj.items():
                traverse(value)

        elif isinstance(obj, list):
            for item in obj:
                traverse(item)

    traverse(prototype_query)
    metadata["From"] = list(set(metadata["From"]))
    return metadata


def parse_report_layout(zip_file: zipfile.ZipFile):
    """
    Parses the Report/Layout file from the PBIX archive.
    Returns a simplified dictionary of pages and visuals with deep metadata.
    """
    try:
        with zip_file.open("Report/Layout") as f:
            content = f.read()
            try:
                layout_json = json.loads(content.decode("utf-16-le"))
            except UnicodeDecodeError:
                layout_json = json.loads(content.decode("utf-8"))

            pages = []
            all_visuals_flat = []

            report_filters = []
            try:
                if "config" in layout_json:
                    root_config = json.loads(layout_json["config"])
                    if "filter" in root_config:
                        report_filters = root_config["filter"]
            except Exception:
                pass

            for section in layout_json.get("sections", []):
                page_name = section.get("displayName", section.get("name", "Unnamed Page"))
                page_id = section.get("name")

                page_width = section.get("width", 1280)
                page_height = section.get("height", 720)

                page_filters = []
                page_style = {}

                try:
                    if "config" in section:
                        page_config = json.loads(section["config"])
                        if "filter" in page_config:
                            page_filters = page_config["filter"]

                        bg_objs = walk_find(page_config, "objects", "background")
                        if bg_objs and len(bg_objs) > 0:
                            bg_props = bg_objs[0].get("properties", {})
                            color_expr = walk_find(bg_props, "color", "solid", "color", "expr")
                            if not color_expr:
                                color_expr = walk_find(bg_props, "color", "expr")

                            trans_expr = walk_find(bg_props, "transparency", "expr")

                            bg_color = resolve_color(color_expr)

                            transparency = parse_transparency(trans_expr, default=0)

                            page_style["background"] = {
                                "color": bg_color,
                                "transparency": transparency,
                            }

                    elif "filter" in section:
                        page_filters = section["filter"]
                except Exception:
                    pass

                visuals = []

                for container in section.get("visualContainers", []):
                    try:
                        layout_info = {
                            "x": container.get("x", 0),
                            "y": container.get("y", 0),
                            "width": container.get("width", 0),
                            "height": container.get("height", 0),
                            "z": container.get("z", 0),
                            "tabOrder": container.get("tabOrder"),
                        }

                        config_str = container.get("config")
                        if config_str:
                            config = json.loads(config_str)
                            single_visual = config.get("singleVisual", {})
                            visual_type = single_visual.get("visualType", "unknown")
                            visual_name = config.get("name", "Unnamed Visual")
                            visual_id_stable = get_visual_id(page_id, visual_name)

                            title = "Untitled"
                            try:
                                vc_objects = single_visual.get("vcObjects", {})
                                title_obj = vc_objects.get("title", [])
                                if title_obj:
                                    title = (
                                        title_obj[0]
                                        .get("properties", {})
                                        .get("text", {})
                                        .get("expr", {})
                                        .get("Literal", {})
                                        .get("Value")
                                    )
                            except Exception:
                                pass

                            if not title or title == "Untitled":
                                try:
                                    objects = single_visual.get("objects", {})
                                    title_obj = objects.get("title", [])
                                    if title_obj:
                                        title = (
                                            title_obj[0]
                                            .get("properties", {})
                                            .get("text", {})
                                            .get("expr", {})
                                            .get("Literal", {})
                                            .get("Value")
                                        )
                                except Exception:
                                    pass

                            if not title:
                                title = "Untitled"

                            visual_style = {}
                            vc_objects = single_visual.get("vcObjects", {})
                            objects = single_visual.get("objects", {})

                            bg_obj_list = vc_objects.get("background", [])
                            if bg_obj_list:
                                bg_props = bg_obj_list[0].get("properties", {})
                                show_expr = (
                                    bg_props.get("show", {})
                                    .get("expr", {})
                                    .get("Literal", {})
                                    .get("Value", "false")
                                )
                                if show_expr == "true":
                                    bg_color_expr = walk_find(bg_props, "color", "solid", "color", "expr")
                                    bg_trans_expr = walk_find(bg_props, "transparency", "expr")
                                    visual_style["background"] = {
                                        "color": resolve_color(bg_color_expr),
                                        "transparency": parse_transparency(bg_trans_expr, default=0),
                                    }

                            title_obj_list = vc_objects.get("title", [])
                            if title_obj_list:
                                title_props = title_obj_list[0].get("properties", {})
                                font_color_expr = walk_find(title_props, "fontColor", "solid", "color", "expr")
                                if font_color_expr:
                                    visual_style.setdefault("title", {})["color"] = resolve_color(font_color_expr)

                            labels_obj_list = objects.get("labels", [])
                            if labels_obj_list:
                                label_props = labels_obj_list[0].get("properties", {})
                                color_expr = walk_find(label_props, "color", "solid", "color", "expr")
                                if color_expr:
                                    visual_style.setdefault("value", {})["color"] = resolve_color(color_expr)

                            cat_labels_obj_list = objects.get("categoryLabels", [])
                            if cat_labels_obj_list:
                                cat_props = cat_labels_obj_list[0].get("properties", {})
                                color_expr = walk_find(cat_props, "color", "solid", "color", "expr")
                                if color_expr:
                                    visual_style.setdefault("categoryLabel", {})["color"] = resolve_color(color_expr)

                            series_colors = []
                            for key in ["dataPoint", "dataColors", "series"]:
                                if key in objects:
                                    for item in objects[key]:
                                        selector = item.get("selector")
                                        props = item.get("properties", {})

                                        color_expr = None
                                        if walk_find(props, "fill", "solid", "color", "expr"):
                                            color_expr = walk_find(props, "fill", "solid", "color", "expr")
                                        elif walk_find(props, "color", "solid", "color", "expr"):
                                            color_expr = walk_find(props, "color", "solid", "color", "expr")
                                        elif walk_find(props, "lineColor", "solid", "color", "expr"):
                                            color_expr = walk_find(props, "lineColor", "solid", "color", "expr")

                                        if color_expr:
                                            series_colors.append({
                                                "selector": selector,
                                                "color": resolve_color(color_expr),
                                            })

                            conditional_formatting = []
                            for obj_key, obj_list in objects.items():
                                for item in obj_list:
                                    props = item.get("properties", {})
                                    for rule_name in [
                                        "fillRule",
                                        "colorRule",
                                        "fontColorRule",
                                        "backgroundColorRule",
                                    ]:
                                        if rule_name in props:
                                            conditional_formatting.append({
                                                "object": obj_key,
                                                "property": rule_name,
                                                "rule": props[rule_name],
                                            })

                                    for color_path in [
                                        ["fill", "solid", "color"],
                                        ["color", "solid", "color"],
                                        ["fontColor", "solid", "color"],
                                    ]:
                                        expr = walk_find(props, *color_path, "expr")
                                        if expr:
                                            if "Literal" not in expr and "ThemeDataColor" not in expr:
                                                conditional_formatting.append({
                                                    "object": obj_key,
                                                    "property": ".".join(color_path),
                                                    "exprType": list(expr.keys())[0] if expr else "Unknown",
                                                    "expr": expr,
                                                })

                            query_meta = {}
                            prototype_query = single_visual.get("prototypeQuery")
                            if prototype_query:
                                query_meta = extract_query_metadata(prototype_query)

                            filters = []
                            filter_json = config.get("filter")
                            if filter_json:
                                filters = filter_json

                            visual_obj = {
                                "id": visual_id_stable,
                                "name": visual_name,
                                "type": visual_type,
                                "title": title,
                                "query_metadata": query_meta,
                                "filters": filters,
                                "layout": layout_info,
                                "style": visual_style,
                                "series_colors": series_colors,
                                "conditional_formatting": conditional_formatting,
                            }
                            visuals.append(visual_obj)
                            all_visuals_flat.append(visual_obj)

                    except Exception as e:
                        logger.warning(f"Failed to parse visual config: {e}")
                        continue

                pages.append({
                    "id": page_id,
                    "name": page_name,
                    "width": page_width,
                    "height": page_height,
                    "filters": page_filters,
                    "style": page_style,
                    "visuals": visuals,
                })

            return {
                "pages": pages,
                "report_filters": report_filters,
                "all_visuals_flat": all_visuals_flat,
                "raw_summary": {
                    "page_count": len(pages),
                    "visual_count": len(all_visuals_flat),
                },
            }

    except KeyError:
        return {"error": "Report/Layout file not found in archive"}
    except json.JSONDecodeError:
        return {"error": "Failed to decode Report/Layout JSON"}
    except Exception as e:
        logger.error(f"Error parsing Report/Layout: {e}")
        return {"error": str(e)}


def parse_data_model(zip_file: zipfile.ZipFile):
    """
    Parses the DataModelSchema file to extract tables, columns, and measures.
    """
    try:
        if "DataModelSchema" in zip_file.namelist():
            with zip_file.open("DataModelSchema") as f:
                content = f.read()
                try:
                    schema_json = json.loads(content.decode("utf-16-le"))
                except UnicodeDecodeError:
                    schema_json = json.loads(content.decode("utf-8"))

                model = schema_json.get("model", {})

                tables = []
                for table in model.get("tables", []):
                    if table.get("isHidden") and table.get("name", "").startswith("LocalDateTable"):
                        continue

                    columns = []
                    for col in table.get("columns", []):
                        col_type = col.get("type", "data")
                        expression = col.get("expression")

                        dependencies = []
                        if col_type == "calculated" or expression:
                            dependencies = extract_dax_dependencies(expression)

                        columns.append({
                            "name": col.get("name"),
                            "dataType": col.get("dataType"),
                            "isHidden": col.get("isHidden", False),
                            "formatString": col.get("formatString"),
                            "type": col_type,
                            "expression": expression,
                            "dependencies": dependencies,
                        })

                    measures = []
                    for measure in table.get("measures", []):
                        expression = measure.get("expression")
                        dependencies = extract_dax_dependencies(expression)

                        measures.append({
                            "name": measure.get("name"),
                            "expression": expression,
                            "isHidden": measure.get("isHidden", False),
                            "formatString": measure.get("formatString"),
                            "dependencies": dependencies,
                        })

                    hierarchies = []
                    for hierarchy in table.get("hierarchies", []):
                        levels = []
                        for level in hierarchy.get("levels", []):
                            levels.append({
                                "name": level.get("name"),
                                "column": level.get("column"),
                            })
                        hierarchies.append({
                            "name": hierarchy.get("name"),
                            "levels": levels,
                            "isHidden": hierarchy.get("isHidden", False),
                        })

                    tables.append({
                        "name": table.get("name"),
                        "columns": columns,
                        "measures": measures,
                        "hierarchies": hierarchies,
                        "isHidden": table.get("isHidden", False),
                        "description": table.get("description"),
                    })

                relationships = []
                for rel in model.get("relationships", []):
                    relationships.append({
                        "fromTable": rel.get("fromTable"),
                        "fromColumn": rel.get("fromColumn"),
                        "toTable": rel.get("toTable"),
                        "toColumn": rel.get("toColumn"),
                        "name": rel.get("name"),
                    })

                return {
                    "tables": tables,
                    "relationships": relationships,
                }
        else:
            return {"error": "DataModelSchema not found", "is_missing": True}

    except Exception as e:
        logger.error(f"Error parsing DataModelSchema: {e}")
        return {"error": str(e)}


def infer_model_from_references(visuals):
    """
    Builds an inferred semantic model (tables, columns, measures)
    from visual query usage when the real schema is missing.
    """
    inferred_tables = {}

    for v in visuals:
        meta = v.get("query_metadata", {})

        for t_name in meta.get("From", []):
            if t_name not in inferred_tables:
                inferred_tables[t_name] = {"columns": {}, "measures": {}}

        current_tables = meta.get("From", [])
        primary_table = current_tables[0] if len(current_tables) == 1 else None

        for sel in meta.get("Select", []):
            name = sel.get("name")
            type_ = sel.get("type")
            inferred_data_type = "string"

            if type_ == "column_agg":
                func = sel.get("func")
                if func in [0, 1, 3, 4]:
                    inferred_data_type = "double"
                elif func == 2:
                    inferred_data_type = "int64"
            elif type_ == "column_hierarchy":
                if sel.get("inferred_type") == "dateTime":
                    inferred_data_type = "dateTime"
            elif type_ == "measure":
                inferred_data_type = "double"

            target_table = primary_table if primary_table else "Inferred_Mixed"

            if target_table not in inferred_tables:
                inferred_tables[target_table] = {"columns": {}, "measures": {}}

            if type_ == "measure":
                inferred_tables[target_table]["measures"][name] = inferred_data_type
            else:
                existing_type = inferred_tables[target_table]["columns"].get(name, "string")
                if inferred_data_type != "string":
                    inferred_tables[target_table]["columns"][name] = inferred_data_type
                elif name not in inferred_tables[target_table]["columns"]:
                    inferred_tables[target_table]["columns"][name] = "string"

    tables_out = []
    for t_name, content in inferred_tables.items():
        tables_out.append({
            "name": t_name,
            "columns": [
                {"name": c, "dataType": t, "type": "inferred"}
                for c, t in content["columns"].items()
            ],
            "measures": [
                {"name": m, "dataType": t, "type": "inferred"}
                for m, t in content["measures"].items()
            ],
            "isHidden": False,
            "description": "Inferred from visual usage",
        })

    return {
        "tables": tables_out,
        "relationships": [],
    }


def build_lineage(visuals, model, queries):
    """
    Constructs the end-to-end lineage graph:
    Visual -> Measure -> Column -> Query Step
    """
    lineage = []

    measure_map = {}
    for t in model.get("tables", []):
        t_name = t["name"]
        for m in t.get("measures", []):
            measure_map[m["name"]] = t_name

    for v in visuals:
        v_id = v["id"]

        meta = v.get("query_metadata", {})

        for sel in meta.get("Select", []):
            name = sel.get("name")
            type_ = sel.get("type")

            if type_ == "measure":
                t_name = measure_map.get(name, "UnknownTable")
                m_id = get_measure_id(t_name, name)

                lineage.append({
                    "from": v_id,
                    "to": m_id,
                    "type": "uses_measure",
                    "label": "uses",
                })

            else:
                from_tables = meta.get("From", [])
                t_name = from_tables[0] if from_tables else "UnknownTable"
                c_id = get_column_id(t_name, name)

                lineage.append({
                    "from": v_id,
                    "to": c_id,
                    "type": "uses_column",
                    "label": "uses",
                })

    for t in model.get("tables", []):
        t_name = t["name"]
        for m in t.get("measures", []):
            m_id = get_measure_id(t_name, m["name"])

            for dep in m.get("dependencies", []):
                if dep["type"] == "column_or_measure":
                    dep_t = dep.get("table", t_name)
                    dep_obj = dep.get("object")

                    c_id = get_column_id(dep_t, dep_obj)
                    lineage.append({
                        "from": m_id,
                        "to": c_id,
                        "type": "depends_on",
                        "label": "references",
                    })

    for q in queries:
        q_name = q["name"]
        q_id = get_query_id(q_name)

        for t in model.get("tables", []):
            if t["name"] == q_name:
                t_id = get_table_id(t["name"])

                lineage.append({
                    "from": q_id,
                    "to": t_id,
                    "type": "produces_table",
                    "label": "loads to",
                })

    return lineage


def parse_mashup(zip_file: zipfile.ZipFile):
    """
    Parses the DataMashup file to extract Power Query (M) scripts.
    """
    try:
        if "DataMashup" not in zip_file.namelist():
            return {"error": "DataMashup not found"}

        with zip_file.open("DataMashup") as f:
            mashup_bytes = f.read()

        zip_signature = b"\x50\x4B\x03\x04"
        zip_offset = mashup_bytes.find(zip_signature)

        if zip_offset == -1:
            return {"error": "Inner ZIP not found in DataMashup"}

        inner_zip_bytes = mashup_bytes[zip_offset:]

        try:
            with zipfile.ZipFile(io.BytesIO(inner_zip_bytes)) as inner_zip:
                m_script = None
                for name in inner_zip.namelist():
                    if name.endswith("Section1.m"):
                        with inner_zip.open(name) as m_file:
                            m_script = m_file.read().decode("utf-8")
                        break

                if m_script:
                    queries = parse_m_script(m_script)
                    return {
                        "m_script": m_script,
                        "queries": queries,
                    }
                return {"error": "Section1.m not found in DataMashup"}

        except zipfile.BadZipFile:
            return {"error": "Failed to open inner ZIP in DataMashup"}

    except Exception as e:
        logger.error(f"Error parsing DataMashup: {e}")
        return {"error": str(e)}


def parse_connections(zip_file: zipfile.ZipFile):
    """
    Parses the Connections file to identify external data sources.
    Useful for thin reports / live connections.
    """
    try:
        if "Connections" not in zip_file.namelist():
            return None

        with zip_file.open("Connections") as f:
            content = f.read()
            try:
                connections_json = json.loads(content.decode("utf-8-sig"))
                return connections_json
            except json.JSONDecodeError:
                return {"error": "Failed to decode Connections JSON"}

    except Exception as e:
        logger.error(f"Error parsing Connections: {e}")
        return {"error": str(e)}
