import {
  compareRelations,
  computeColorValues,
  DeploymentElement,
  isScopedElementView,
  LikeC4Model,
  parentFqn,
  sortByFqnHierarchically
} from "@likec4/core";
import { resolveRulesExtendedViews } from "@likec4/core/compute-view";
import { deepEqual as eq } from "fast-equals";
import { Disposable, DocumentState, interruptAndCheck } from "langium";
import {
  filter,
  flatMap,
  groupBy,
  indexBy,
  isBoolean,
  isDefined,
  isEmpty,
  isNonNullish,
  isNullish,
  isNumber,
  isTruthy,
  map,
  mapToObj,
  mapValues,
  omit,
  pipe,
  prop,
  reduce,
  reverse,
  sort,
  values
} from "remeda";
import { isParsedLikeC4LangiumDocument } from "../ast.js";
import { logger, logWarnError } from "../logger.js";
import { ADisposable } from "../utils/index.js";
import { assignNavigateTo, resolveRelativePaths } from "../view-utils/index.js";
function buildModel(services, docs) {
  const c4Specification = {
    tags: /* @__PURE__ */ new Set(),
    deployments: {},
    elements: {},
    relationships: {},
    colors: {}
  };
  const globals = {
    predicates: {},
    dynamicPredicates: {},
    styles: {}
  };
  for (const doc of docs) {
    const {
      c4Specification: spec,
      c4Globals
    } = doc;
    spec.tags.forEach((t) => c4Specification.tags.add(t));
    Object.assign(c4Specification.elements, spec.elements);
    Object.assign(c4Specification.relationships, spec.relationships);
    Object.assign(c4Specification.colors, spec.colors);
    Object.assign(c4Specification.deployments, spec.deployments);
    Object.assign(globals.predicates, c4Globals.predicates);
    Object.assign(globals.dynamicPredicates, c4Globals.dynamicPredicates);
    Object.assign(globals.styles, c4Globals.styles);
  }
  function resolveLinks(doc, links) {
    return map(
      links,
      (link) => {
        try {
          const relative = services.lsp.DocumentLinkProvider.relativeLink(doc, link.url);
          if (relative && relative !== link.url) {
            return {
              ...link,
              relative
            };
          }
        } catch (e) {
          logWarnError(e);
        }
        return link;
      }
    );
  }
  const customColorDefinitions = mapValues(
    c4Specification.colors,
    (c) => computeColorValues(c.color)
  );
  function toModelElement(doc) {
    return ({
      tags,
      links: unresolvedLinks,
      style: {
        color,
        shape,
        icon,
        opacity,
        border,
        multiple
      },
      id,
      kind,
      title,
      description,
      technology,
      metadata
    }) => {
      try {
        const __kind = c4Specification.elements[kind];
        if (!__kind) {
          logger.warn(`No kind '${kind}' found for ${id}`);
          return null;
        }
        const links = unresolvedLinks ? resolveLinks(doc, unresolvedLinks) : null;
        color ??= __kind.style.color;
        shape ??= __kind.style.shape;
        icon ??= __kind.style.icon;
        opacity ??= __kind.style.opacity;
        border ??= __kind.style.border;
        technology ??= __kind.technology;
        multiple ??= __kind.style.multiple;
        return {
          ...color && { color },
          ...shape && { shape },
          ...icon && { icon },
          ...metadata && !isEmpty(metadata) && { metadata },
          ...__kind.notation && { notation: __kind.notation },
          style: {
            ...border && { border },
            ...isBoolean(multiple) && { multiple },
            ...isNumber(opacity) && { opacity }
          },
          links,
          tags: tags ?? null,
          technology: technology ?? null,
          description: description ?? null,
          title,
          kind,
          id
        };
      } catch (e) {
        logWarnError(e);
      }
      return null;
    };
  }
  const elements = pipe(
    docs,
    flatMap((d) => map(d.c4Elements, toModelElement(d))),
    filter(isTruthy),
    // sort from root elements to nested, so that parent is always present
    // Import to preserve the order from the source
    sortByFqnHierarchically,
    reduce(
      (acc, el) => {
        const parent = parentFqn(el.id);
        if (parent && isNullish(acc[parent])) {
          logWarnError(`No parent found for ${el.id}`);
          return acc;
        }
        acc[el.id] = el;
        return acc;
      },
      {}
    )
  );
  function toModelRelation(doc) {
    return ({
      astPath,
      source,
      target,
      kind,
      links: unresolvedLinks,
      id,
      ...model
    }) => {
      if (isNullish(elements[source]) || isNullish(elements[target])) {
        logger.warn(
          `Invalid relation ${id} at ${doc.uri.path} ${astPath}, source: ${source}(${!!elements[source]}), target: ${target}(${!!elements[target]})`
        );
        return null;
      }
      const links = unresolvedLinks ? resolveLinks(doc, unresolvedLinks) : null;
      if (isNonNullish(kind) && kind in c4Specification.relationships) {
        return {
          ...c4Specification.relationships[kind],
          ...model,
          ...links && { links },
          source,
          target,
          kind,
          id
        };
      }
      return {
        ...links && { links },
        ...model,
        source,
        target,
        id
      };
    };
  }
  const relations = pipe(
    docs,
    flatMap((d) => map(d.c4Relations, toModelRelation(d))),
    filter(isTruthy),
    sort(compareRelations),
    reverse(),
    indexBy(prop("id"))
  );
  function toDeploymentElement(doc) {
    return (parsed) => {
      if (!DeploymentElement.isDeploymentNode(parsed)) {
        if (!parsed.links || parsed.links.length === 0) {
          return parsed;
        }
        const links = resolveLinks(doc, parsed.links);
        return {
          ...parsed,
          links
        };
      }
      try {
        const __kind = c4Specification.deployments[parsed.kind];
        if (!__kind) {
          logger.warn(`No kind '${parsed.kind}' found for ${parsed.id}`);
          return null;
        }
        let {
          technology = __kind.technology,
          notation = __kind.notation,
          links,
          style
        } = parsed;
        return {
          ...parsed,
          ...notation && { notation },
          ...technology && { technology },
          style: {
            border: "dashed",
            opacity: 10,
            ...__kind.style,
            ...style
          },
          links: links ? resolveLinks(doc, links) : null
        };
      } catch (e) {
        logWarnError(e);
      }
      return null;
    };
  }
  const deploymentElements = pipe(
    docs,
    flatMap((d) => map(d.c4Deployments, toDeploymentElement(d))),
    filter(isTruthy),
    // sort from root elements to nested, so that parent is always present
    // Import to preserve the order from the source
    sortByFqnHierarchically,
    reduce(
      (acc, el) => {
        const parent = parentFqn(el.id);
        if (parent && isNullish(acc[parent])) {
          logWarnError(`No parent found for deployment element ${el.id}`);
          return acc;
        }
        acc[el.id] = el;
        return acc;
      },
      {}
    )
  );
  function toDeploymentRelation(doc) {
    return ({
      astPath,
      source,
      target,
      kind,
      links: unresolvedLinks,
      id,
      ...model
    }) => {
      if (isNullish(deploymentElements[source.id]) || isNullish(deploymentElements[target.id])) {
        logger.warn(
          `Invalid deployment relation ${id} at ${doc.uri.path} ${astPath}, source: ${source.id}(${!!deploymentElements[source.id]}), target: ${target.id}(${!!deploymentElements[target.id]})`
        );
        return null;
      }
      const links = unresolvedLinks ? resolveLinks(doc, unresolvedLinks) : null;
      if (isNonNullish(kind) && kind in c4Specification.relationships) {
        return {
          ...c4Specification.relationships[kind],
          ...model,
          ...links && { links },
          source,
          target,
          kind,
          id
        };
      }
      return {
        ...links && { links },
        ...model,
        source,
        target,
        id
      };
    };
  }
  const deploymentRelations = pipe(
    docs,
    flatMap((d) => map(d.c4DeploymentRelations, toDeploymentRelation(d))),
    filter(isTruthy),
    reduce(
      (acc, el) => {
        if (isDefined(acc[el.id])) {
          logWarnError(`Duplicate deployment relation ${el.id}`);
          return acc;
        }
        acc[el.id] = el;
        return acc;
      },
      {}
    )
  );
  function toC4View(doc) {
    const docUri = doc.uri.toString();
    return (parsedAstView) => {
      let {
        id,
        title,
        description,
        tags,
        links: unresolvedLinks,
        // ignore this property
        astPath: _ignore,
        // model should include discriminant __
        ...model
      } = parsedAstView;
      if (parsedAstView.__ === "element" && isNullish(title) && "viewOf" in parsedAstView) {
        title = elements[parsedAstView.viewOf]?.title ?? null;
      }
      if (isNullish(title) && id === "index") {
        title = "Landscape view";
      }
      const links = unresolvedLinks ? resolveLinks(doc, unresolvedLinks) : null;
      return {
        ...model,
        customColorDefinitions,
        tags,
        links,
        docUri,
        description,
        title,
        id
      };
    };
  }
  const parsedViews = pipe(
    docs,
    flatMap((d) => map(d.c4Views, toC4View(d))),
    // Resolve relative paths and sort by
    resolveRelativePaths
  );
  if (!parsedViews.some((v) => v.id === "index")) {
    parsedViews.unshift({
      __: "element",
      id: "index",
      title: "Landscape view",
      description: null,
      tags: null,
      links: null,
      customColorDefinitions,
      rules: [
        {
          include: [
            {
              wildcard: true
            }
          ]
        }
      ]
    });
  }
  const views = pipe(
    parsedViews,
    indexBy(prop("id")),
    resolveRulesExtendedViews
  );
  return {
    specification: {
      tags: Array.from(c4Specification.tags),
      elements: c4Specification.elements,
      relationships: c4Specification.relationships,
      deployments: c4Specification.deployments
    },
    elements,
    relations,
    globals,
    views,
    deployments: {
      elements: deploymentElements,
      relations: deploymentRelations
    }
  };
}
const CACHE_KEY_PARSED_MODEL = "ParsedLikeC4Model";
const CACHE_KEY_COMPUTED_MODEL = "ComputedLikeC4Model";
export class LikeC4ModelBuilder extends ADisposable {
  constructor(services) {
    super();
    this.services = services;
    this.langiumDocuments = services.shared.workspace.LangiumDocuments;
    const parser = services.likec4.ModelParser;
    this.onDispose(
      services.shared.workspace.DocumentBuilder.onUpdate((_changed, deleted) => {
        if (deleted.length > 0) {
          this.notifyListeners(deleted);
        }
      })
    );
    this.onDispose(
      services.shared.workspace.DocumentBuilder.onBuildPhase(
        DocumentState.Validated,
        async (docs, _cancelToken) => {
          let parsed = [];
          logger.debug(`[ModelBuilder] onValidated (${docs.length} docs)`);
          for (const doc of docs) {
            try {
              parsed.push(parser.parse(doc).uri);
            } catch (e) {
              logWarnError(e);
            }
          }
          await interruptAndCheck(_cancelToken);
          if (parsed.length > 0) {
            this.notifyListeners(parsed);
          }
        }
      )
    );
    logger.debug(`[ModelBuilder] Created`);
  }
  langiumDocuments;
  listeners = [];
  /**
   * WARNING:
   * This method is internal and should to be called only when all documents are known to be parsed.
   * Otherwise, the model may be incomplete.
   */
  unsafeSyncBuildModel() {
    const docs = this.documents();
    if (docs.length === 0) {
      logger.debug("[ModelBuilder] No documents to build model from");
      return null;
    }
    const cache = this.services.WorkspaceCache;
    return cache.get(CACHE_KEY_PARSED_MODEL, () => {
      logger.debug(`[ModelBuilder] buildModel (${docs.length} docs)`);
      return buildModel(this.services, docs);
    });
  }
  async buildModel(cancelToken) {
    const cache = this.services.WorkspaceCache;
    const cached = cache.get(CACHE_KEY_PARSED_MODEL);
    if (cached) {
      return cached;
    }
    return await this.services.shared.workspace.WorkspaceLock.read(async () => {
      if (cancelToken) {
        await interruptAndCheck(cancelToken);
      }
      return this.unsafeSyncBuildModel();
    });
  }
  previousViews = {};
  /**
   * WARNING:
   * This method is internal and should to be called only when all documents are known to be parsed.
   * Otherwise, the model may be incomplete.
   */
  unsafeSyncBuildComputedModel(model) {
    const cache = this.services.WorkspaceCache;
    const viewsCache = this.services.WorkspaceCache;
    return cache.get(CACHE_KEY_COMPUTED_MODEL, () => {
      const computeView = LikeC4Model.makeCompute(model);
      const allViews = [];
      for (const view of values(model.views)) {
        const result = computeView(view);
        if (!result.isSuccess) {
          logWarnError(result.error);
          continue;
        }
        allViews.push(result.view);
      }
      assignNavigateTo(allViews);
      const views = mapToObj(allViews, (v) => {
        const previous = this.previousViews[v.id];
        const view = previous && eq(v, previous) ? previous : v;
        viewsCache.set(computedViewKey(v.id), view);
        return [v.id, view];
      });
      this.previousViews = { ...views };
      return {
        ...omit(model, ["views"]),
        views
      };
    });
  }
  async buildComputedModel(cancelToken) {
    const cache = this.services.WorkspaceCache;
    if (cache.has(CACHE_KEY_COMPUTED_MODEL)) {
      return cache.get(CACHE_KEY_COMPUTED_MODEL);
    }
    return await this.services.shared.workspace.WorkspaceLock.read(async () => {
      if (cancelToken) {
        await interruptAndCheck(cancelToken);
      }
      const model = this.unsafeSyncBuildModel();
      if (!model) {
        return null;
      }
      return this.unsafeSyncBuildComputedModel(model);
    });
  }
  async computeView(viewId, cancelToken) {
    const cache = this.services.WorkspaceCache;
    const cacheKey = computedViewKey(viewId);
    if (cache.has(cacheKey)) {
      return cache.get(cacheKey);
    }
    return await this.services.shared.workspace.WorkspaceLock.read(async () => {
      if (cancelToken) {
        await interruptAndCheck(cancelToken);
      }
      return cache.get(cacheKey, () => {
        const model = this.unsafeSyncBuildModel();
        const view = model?.views[viewId];
        if (!view) {
          logger.warn(`[ModelBuilder] Cannot find view ${viewId}`);
          return null;
        }
        const result = LikeC4Model.makeCompute(model)(view);
        if (!result.isSuccess) {
          logWarnError(result.error);
          return null;
        }
        let computedView = result.view;
        const allElementViews = pipe(
          model.views,
          values(),
          filter(isScopedElementView),
          filter((v) => v.id !== viewId),
          groupBy((v) => v.viewOf)
        );
        for (const node of computedView.nodes) {
          if (!node.navigateTo) {
            const viewsOfNode = allElementViews[node.id];
            if (viewsOfNode) {
              node.navigateTo = viewsOfNode[0].id;
            }
          }
        }
        const previous = this.previousViews[viewId];
        if (previous && eq(computedView, previous)) {
          computedView = previous;
        } else {
          this.previousViews[viewId] = computedView;
        }
        return computedView;
      });
    });
  }
  onModelParsed(callback) {
    this.listeners.push(callback);
    return Disposable.create(() => {
      const index = this.listeners.indexOf(callback);
      if (index >= 0) {
        this.listeners.splice(index, 1);
      }
    });
  }
  documents() {
    return this.langiumDocuments.all.filter(isParsedLikeC4LangiumDocument).toArray();
  }
  notifyListeners(docs) {
    for (const listener of this.listeners) {
      try {
        listener(docs);
      } catch (e) {
        logWarnError(e);
      }
    }
  }
}
function computedViewKey(viewId) {
  return `computed-view-${viewId}`;
}
