#include "compiler/pipeline_parser.h"

#include <fstream>
#include <nlohmann/json.hpp>

#include "compiler/jsonld_loader.h"

namespace cf {

namespace {

using json = nlohmann::json;

std::vector<json> to_list(const json& node) {
  std::vector<json> out;
  if (node.is_array()) {
    for (const auto& item : node) {
      out.push_back(item);
    }
  } else if (node.is_object()) {
    out.push_back(node);
  }
  return out;
}

json deref(const json& node, const std::unordered_map<std::string, json>& by_id) {
  if (node.is_object() && node.contains("@id")) {
    std::string id = node["@id"].get<std::string>();
    auto it = by_id.find(id);
    if (it != by_id.end()) {
      if (!node.contains("cf:nodeDefinition") && !node.contains("cf:sourceEndpoint") &&
          !node.contains("cf:targetEndpoint") && !node.contains("cf:bindsParameter")) {
        return it->second;
      }
    }
  }
  return node;
}

std::string normalize_param_value(const json& value) {
  if (value.is_object() && value.contains("@type") && value.contains("@value")) {
    if (value["@type"].is_string() && value["@type"].get<std::string>() == "rdf:JSON") {
      if (value["@value"].is_string()) {
        return value["@value"].get<std::string>();
      }
    }
  }
  return value.dump();
}

std::string extract_id_or_string(const json& node) {
  if (node.is_string()) {
    return node.get<std::string>();
  }
  if (node.is_object() && node.contains("@id") && node["@id"].is_string()) {
    return node["@id"].get<std::string>();
  }
  return {};
}

std::string dirname_of(const std::string& path) {
  size_t pos = path.find_last_of("/\\");
  if (pos == std::string::npos) return "";
  return path.substr(0, pos);
}

void append_steps_paths(const json& header, const std::string& base_dir, std::vector<std::string>& out) {
  if (!header.contains("cf:stepsPath")) {
    return;
  }
  const auto& paths = header["cf:stepsPath"];
  if (paths.is_array()) {
    for (const auto& entry : paths) {
      if (entry.is_string()) {
        std::string path = entry.get<std::string>();
        if (!base_dir.empty() && path.rfind("http", 0) != 0 && path.rfind("https", 0) != 0 &&
            path.rfind("file:", 0) != 0) {
          path = base_dir + "/" + path;
        }
        out.push_back(path);
      }
    }
  } else if (paths.is_string()) {
    std::string path = paths.get<std::string>();
    if (!base_dir.empty() && path.rfind("http", 0) != 0 && path.rfind("https", 0) != 0 &&
        path.rfind("file:", 0) != 0) {
      path = base_dir + "/" + path;
    }
    out.push_back(path);
  }
}

void append_plugin_paths(const json& header, const std::string& base_dir, std::vector<std::string>& out) {
  if (!header.contains("cf:pluginPath")) {
    return;
  }
  const auto& paths = header["cf:pluginPath"];
  if (paths.is_array()) {
    for (const auto& entry : paths) {
      if (entry.is_string()) {
        std::string path = entry.get<std::string>();
        if (!base_dir.empty() && path.rfind("http", 0) != 0 && path.rfind("https", 0) != 0 &&
            path.rfind("file:", 0) != 0) {
          path = base_dir + "/" + path;
        }
        out.push_back(path);
      }
    }
  } else if (paths.is_string()) {
    std::string path = paths.get<std::string>();
    if (!base_dir.empty() && path.rfind("http", 0) != 0 && path.rfind("https", 0) != 0 &&
        path.rfind("file:", 0) != 0) {
      path = base_dir + "/" + path;
    }
    out.push_back(path);
  }
}

cf::JsonLdContext parse_context(const json& ctx_node, const std::string& base_dir, std::string& error) {
  cf::JsonLdContext ctx;
  if (ctx_node.is_object()) {
    for (auto it = ctx_node.begin(); it != ctx_node.end(); ++it) {
      if (it.value().is_string()) {
        ctx.prefixes[it.key()] = it.value().get<std::string>();
      }
    }
    return ctx;
  }
  if (!ctx_node.is_array()) {
    return ctx;
  }
  for (const auto& entry : ctx_node) {
    if (entry.is_object()) {
      for (auto it = entry.begin(); it != entry.end(); ++it) {
        if (it.value().is_string()) {
          ctx.prefixes[it.key()] = it.value().get<std::string>();
        }
      }
    } else if (entry.is_string()) {
      std::string ref = entry.get<std::string>();
      std::string path = ref;
      if (!base_dir.empty() && ref.rfind("http", 0) != 0 && ref.rfind("https", 0) != 0) {
        path = base_dir + "/" + ref;
      }
      json ref_json;
      std::ifstream fh(path);
      if (!fh) {
        error = "Failed to open context file: " + path;
        return ctx;
      }
      try {
        fh >> ref_json;
      } catch (const std::exception& ex) {
        error = std::string("Failed to parse context file: ") + ex.what();
        return ctx;
      }
      if (ref_json.contains("@context")) {
        cf::JsonLdContext nested = parse_context(ref_json["@context"], base_dir, error);
        ctx.prefixes.insert(nested.prefixes.begin(), nested.prefixes.end());
      }
    }
  }
  return ctx;
}

}  // namespace

bool load_pipeline_spec(const std::string& path, PipelineSpec& out, std::string& error) {
  std::string base_dir = dirname_of(path);
  std::ifstream fh(path);
  if (!fh) {
    error = "Failed to open pipeline file: " + path;
    return false;
  }
  json doc;
  try {
    fh >> doc;
  } catch (const std::exception& ex) {
    error = std::string("Failed to parse pipeline JSON: ") + ex.what();
    return false;
  }
  cf::JsonLdContext ctx;
  if (doc.contains("@context")) {
    ctx = parse_context(doc["@context"], base_dir, error);
    if (!error.empty()) {
      return false;
    }
  }
  json pipeline_doc = doc;
  std::unordered_map<std::string, json> by_id;
  if (doc.contains("@graph") && doc["@graph"].is_array()) {
    for (const auto& entry : doc["@graph"]) {
      if (entry.contains("@id")) {
        by_id[entry["@id"].get<std::string>()] = entry;
      }
    }
    for (const auto& entry : doc["@graph"]) {
      if (entry.contains("cf:hasNode") || entry.contains("cf:hasPipelineNode")) {
        pipeline_doc = entry;
        break;
      }
      if (entry.contains("@type") && entry["@type"].is_string()) {
        if (cf::expand_iri(ctx, entry["@type"].get<std::string>()) == "https://cogniflow.org/ns#ProcessingPipeline") {
          pipeline_doc = entry;
          break;
        }
      } else if (entry.contains("@type") && entry["@type"].is_array()) {
        for (const auto& t : entry["@type"]) {
          if (t.is_string() &&
              cf::expand_iri(ctx, t.get<std::string>()) == "https://cogniflow.org/ns#ProcessingPipeline") {
            pipeline_doc = entry;
            break;
          }
        }
      }
    }
  }

  if (pipeline_doc.contains("cf:hasNode")) {
    for (const auto& node : to_list(pipeline_doc["cf:hasNode"])) {
      json node_obj = deref(node, by_id);
      if (!node_obj.contains("@id") || !node_obj.contains("cf:nodeDefinition")) {
        error = "Pipeline node missing @id or cf:nodeDefinition";
        return false;
      }
      PipelineNodeSpec spec;
      spec.id = node_obj["@id"].get<std::string>();
      spec.step_iri = cf::expand_iri(ctx, extract_id_or_string(node_obj["cf:nodeDefinition"]));
      if (node_obj.contains("cf:hasParameter")) {
        for (const auto& param : to_list(node_obj["cf:hasParameter"])) {
          if (param.contains("cf:parameterName") && param.contains("cf:parameterValue")) {
            spec.parameters_json[param["cf:parameterName"].get<std::string>()] =
              normalize_param_value(param["cf:parameterValue"]);
          }
        }
      }
      out.nodes.push_back(spec);
    }
  } else if (pipeline_doc.contains("cf:hasPipelineNode")) {
    for (const auto& node : to_list(pipeline_doc["cf:hasPipelineNode"])) {
      json node_obj = deref(node, by_id);
      if (!node_obj.contains("@id") || !node_obj.contains("cf:nodeDefinition")) {
        error = "Pipeline node missing @id or cf:nodeDefinition";
        return false;
      }
      PipelineNodeSpec spec;
      spec.id = node_obj["@id"].get<std::string>();
      spec.step_iri = cf::expand_iri(ctx, extract_id_or_string(node_obj["cf:nodeDefinition"]));
      if (node_obj.contains("cf:hasParameterBinding")) {
        for (const auto& binding_ref : to_list(node_obj["cf:hasParameterBinding"])) {
          json binding = deref(binding_ref, by_id);
          if (binding.contains("cf:bindsParameter") && binding["cf:bindsParameter"].contains("@id") &&
              binding.contains("cf:value")) {
            std::string param_iri = cf::expand_iri(ctx, binding["cf:bindsParameter"]["@id"].get<std::string>());
            spec.parameters_json_by_iri[param_iri] = normalize_param_value(binding["cf:value"]);
          }
        }
      }
      out.nodes.push_back(spec);
    }
  } else {
    error = "Pipeline missing cf:hasNode or cf:hasPipelineNode";
    return false;
  }

  if (pipeline_doc.contains("cf:hasConnection")) {
    for (const auto& conn_ref : to_list(pipeline_doc["cf:hasConnection"])) {
      json conn = deref(conn_ref, by_id);
      PipelineConnectionSpec spec;
      if (conn.contains("cf:fromNode") && conn.contains("cf:toNode")) {
        spec.from_node = conn["cf:fromNode"].get<std::string>();
        spec.to_node = conn["cf:toNode"].get<std::string>();
        if (conn.contains("cf:fromPort") && conn["cf:fromPort"].contains("cf:endpointPortKey")) {
          spec.from_port_key = cf::expand_iri(ctx, extract_id_or_string(conn["cf:fromPort"]["cf:endpointPortKey"]));
        }
        if (conn.contains("cf:toPort") && conn["cf:toPort"].contains("cf:endpointPortKey")) {
          spec.to_port_key = cf::expand_iri(ctx, extract_id_or_string(conn["cf:toPort"]["cf:endpointPortKey"]));
        }
        out.connections.push_back(spec);
        continue;
      }

      if (conn.contains("cf:sourceEndpoint") && conn.contains("cf:targetEndpoint")) {
        auto src = deref(conn["cf:sourceEndpoint"], by_id);
        auto dst = deref(conn["cf:targetEndpoint"], by_id);
        if (!src.contains("cf:inNode") || !dst.contains("cf:inNode")) {
          error = "Pipeline connection missing cf:inNode";
          return false;
        }
        spec.from_node = src["cf:inNode"]["@id"].get<std::string>();
        spec.to_node = dst["cf:inNode"]["@id"].get<std::string>();
        if (src.contains("cf:endpointPortKey")) {
          spec.from_port_key = cf::expand_iri(ctx, extract_id_or_string(src["cf:endpointPortKey"]));
        }
        if (dst.contains("cf:endpointPortKey")) {
          spec.to_port_key = cf::expand_iri(ctx, extract_id_or_string(dst["cf:endpointPortKey"]));
        }
        if (dst.contains("cf:endpointLabel")) {
          spec.to_port_label = dst["cf:endpointLabel"].get<std::string>();
        }
        out.connections.push_back(spec);
        continue;
      }

      error = "Pipeline connection missing cf:fromNode/cf:toNode or endpoints";
      return false;
    }
  }
  if (pipeline_doc.contains("cf:hasStepsHeader")) {
    for (const auto& header_ref : to_list(pipeline_doc["cf:hasStepsHeader"])) {
      json header = deref(header_ref, by_id);
      append_steps_paths(header, base_dir, out.steps_files);
    }
  }
  if (pipeline_doc.contains("cf:hasPluginsHeader")) {
    for (const auto& header_ref : to_list(pipeline_doc["cf:hasPluginsHeader"])) {
      json header = deref(header_ref, by_id);
      append_plugin_paths(header, base_dir, out.plugin_dirs);
    }
  }
  return true;
}

}  // namespace cf
