import { rootPath } from "./config";
import { readYamlRecord } from "./platform-infra-ops-library";
import { materializeYamlComposition } from "./yaml-composition";

const pacConfigPath = "config/platform-infra/pipelines-as-code.yaml";
const giteaConfigPath = "config/platform-infra/gitea.yaml";

export const PAC_AUTOMATIC_DELIVERY_REFERENCE = "docs/reference/platform-infra.md#gitea-与-pipelines-as-code-边界";

export interface PacDeliveryConsumerAuthority {
  readonly consumerId: string;
  readonly node: string;
  readonly lane: string;
  readonly namespace: string;
  readonly pipelineRunPrefix: string;
  readonly repositoryRef: string;
  readonly sourceRepository: string;
  readonly sourceBranch: string;
  readonly sourceSnapshotPrefix: string;
  readonly giteaOwner: string;
  readonly giteaRepository: string;
  readonly giteaRepositoryUrl: string;
  readonly pacControllerVersion: string;
  readonly configRefs: readonly [string, string];
}

export interface CicdDeliveryAuthorityCatalog {
  readonly consumers: readonly PacDeliveryConsumerAuthority[];
  readonly configRefs: readonly [string, string];
}

export interface CicdDeliveryAuthorityQuery {
  readonly consumerId?: string | null;
  readonly node?: string | null;
  readonly lane?: string | null;
  readonly sourceRepository?: string | null;
  readonly sourceBranch?: string | null;
  readonly declaredSourceAuthorityMode?: string | null;
  readonly declaredConfigRef?: string | null;
}

export interface CicdDeliveryMutationContext {
  readonly command: string;
  readonly statusCommand: string;
  readonly action: string;
  readonly node: string;
  readonly lane: string;
}

export type CicdDeliveryMutationDecision =
  | {
      readonly allowed: true;
      readonly authority: Extract<CicdDeliveryAuthority, { kind: "legacy-manual" }>;
    }
  | {
      readonly allowed: false;
      readonly authority: Exclude<CicdDeliveryAuthority, { kind: "legacy-manual" }>;
      readonly result: Record<string, unknown>;
    };

export type CicdDeliveryAuthority =
  | {
      readonly kind: "pac-pr-merge";
      readonly migrated: true;
      readonly mutationHintsAllowed: false;
      readonly consumer: PacDeliveryConsumerAuthority;
      readonly reason: "exact-pac-consumer";
      readonly valuesPrinted: false;
    }
  | {
      readonly kind: "legacy-manual";
      readonly migrated: false;
      readonly mutationHintsAllowed: true;
      readonly consumer: null;
      readonly reason: "explicit-legacy-source-authority";
      readonly declaredSourceAuthorityMode: string;
      readonly declaredConfigRef: string;
      readonly valuesPrinted: false;
    }
  | {
      readonly kind: "unknown";
      readonly migrated: false;
      readonly mutationHintsAllowed: false;
      readonly consumer: null;
      readonly reason: "authority-config-invalid" | "authority-identity-mismatch" | "authority-not-declared" | "authority-ambiguous";
      readonly detail: string;
      readonly configRefs: readonly string[];
      readonly valuesPrinted: false;
    };

interface PacRepositoryRow {
  readonly id: string;
  readonly owner: string;
  readonly repo: string;
  readonly sourceBranch: string;
  readonly sourceSnapshotPrefix: string;
  readonly url: string;
}

interface GiteaRepositoryRow {
  readonly targetId: string;
  readonly owner: string;
  readonly repo: string;
  readonly sourceRepository: string;
  readonly sourceBranch: string;
}

const legacyAuthorityModes = new Set(["gitMirrorSnapshot", "git-mirror-snapshot", "k8s-git-mirror-snapshot"]);

export function readCicdDeliveryAuthorityCatalog(): CicdDeliveryAuthorityCatalog {
  const pacRoot = composedYaml(pacConfigPath, "platform-infra-pipelines-as-code");
  const giteaRoot = composedYaml(giteaConfigPath, "platform-infra-gitea");
  const release = record(pacRoot.release, `${pacConfigPath}#release`);
  const pacControllerVersion = requiredString(release.version, `${pacConfigPath}#release.version`);
  const pacRepositories = recordArray(pacRoot.repositories, `${pacConfigPath}#repositories`).map((item, index) => {
    const path = `${pacConfigPath}#repositories[${index}]`;
    const params = record(item.params, `${path}.params`);
    return {
      id: requiredString(item.id ?? item.name, `${path}.id`),
      owner: requiredString(item.owner, `${path}.owner`),
      repo: requiredString(item.repo, `${path}.repo`),
      sourceBranch: requiredString(params.source_branch, `${path}.params.source_branch`),
      sourceSnapshotPrefix: requiredString(params.source_snapshot_prefix, `${path}.params.source_snapshot_prefix`),
      url: requiredString(item.url, `${path}.url`),
    } satisfies PacRepositoryRow;
  });
  const sourceAuthority = record(giteaRoot.sourceAuthority, `${giteaConfigPath}#sourceAuthority`);
  const giteaRepositories = recordArray(sourceAuthority.repositories, `${giteaConfigPath}#sourceAuthority.repositories`).map((item, index) => {
    const path = `${giteaConfigPath}#sourceAuthority.repositories[${index}]`;
    const upstream = record(item.upstream, `${path}.upstream`);
    const gitea = record(item.gitea, `${path}.gitea`);
    return {
      targetId: requiredString(item.targetId, `${path}.targetId`),
      owner: requiredString(gitea.owner, `${path}.gitea.owner`),
      repo: requiredString(gitea.name, `${path}.gitea.name`),
      sourceRepository: requiredString(upstream.repository, `${path}.upstream.repository`),
      sourceBranch: requiredString(upstream.branch, `${path}.upstream.branch`),
    } satisfies GiteaRepositoryRow;
  });
  const consumers = recordArray(pacRoot.consumers, `${pacConfigPath}#consumers`).map((item, index) => {
    const path = `${pacConfigPath}#consumers[${index}]`;
    const consumerId = requiredString(item.id, `${path}.id`);
    const node = requiredString(item.node, `${path}.node`);
    const lane = requiredString(item.lane, `${path}.lane`);
    const namespace = requiredString(item.namespace, `${path}.namespace`);
    const pipelineRunPrefix = requiredString(item.pipelineRunPrefix, `${path}.pipelineRunPrefix`);
    const repositoryRef = requiredString(item.repositoryRef, `${path}.repositoryRef`);
    const consumerParams = item.params === undefined ? {} : record(item.params, `${path}.params`);
    const pacRepository = exactlyOne(
      pacRepositories.filter((candidate) => same(candidate.id, repositoryRef)),
      `${path}.repositoryRef=${repositoryRef}`,
    );
    const sourceBranch = requiredString(
      consumerParams.source_branch ?? pacRepository.sourceBranch,
      `${path}.params.source_branch`,
    );
    const sourceSnapshotPrefix = requiredString(
      consumerParams.source_snapshot_prefix ?? pacRepository.sourceSnapshotPrefix,
      `${path}.params.source_snapshot_prefix`,
    );
    const authorityRepository = exactlyOne(
      giteaRepositories.filter((candidate) => same(candidate.targetId, node)
        && same(candidate.owner, pacRepository.owner)
        && same(candidate.repo, pacRepository.repo)
        && same(candidate.sourceBranch, sourceBranch)),
      `${path} source authority ${node}/${pacRepository.owner}/${pacRepository.repo}@${sourceBranch}`,
    );
    return {
      consumerId,
      node,
      lane,
      namespace,
      pipelineRunPrefix,
      repositoryRef,
      sourceRepository: authorityRepository.sourceRepository,
      sourceBranch,
      sourceSnapshotPrefix,
      giteaOwner: pacRepository.owner,
      giteaRepository: pacRepository.repo,
      giteaRepositoryUrl: pacRepository.url,
      pacControllerVersion,
      configRefs: [pacConfigPath, giteaConfigPath],
    } satisfies PacDeliveryConsumerAuthority;
  });
  const duplicateIds = consumers.filter((item, index) => consumers.findIndex((candidate) => same(candidate.consumerId, item.consumerId)) !== index);
  if (duplicateIds.length > 0) throw new Error(`${pacConfigPath} contains duplicate consumer ids: ${duplicateIds.map((item) => item.consumerId).join(", ")}`);
  return { consumers, configRefs: [pacConfigPath, giteaConfigPath] };
}

export function resolveCicdDeliveryAuthority(query: CicdDeliveryAuthorityQuery): CicdDeliveryAuthority {
  try {
    return resolveCicdDeliveryAuthorityFromCatalog(readCicdDeliveryAuthorityCatalog(), query);
  } catch (error) {
    return {
      kind: "unknown",
      migrated: false,
      mutationHintsAllowed: false,
      consumer: null,
      reason: "authority-config-invalid",
      detail: error instanceof Error ? error.message : String(error),
      configRefs: [pacConfigPath, giteaConfigPath],
      valuesPrinted: false,
    };
  }
}

export function resolveCicdDeliveryAuthorityFromCatalog(catalog: CicdDeliveryAuthorityCatalog, query: CicdDeliveryAuthorityQuery): CicdDeliveryAuthority {
  const suppliedIdentity = [query.consumerId, query.node, query.lane, query.sourceRepository, query.sourceBranch]
    .some((value) => normalized(value) !== null);
  const candidates = suppliedIdentity ? catalog.consumers.filter((consumer) => matchesQuery(consumer, query)) : [];
  const stableCandidates = stableIdentityCandidates(catalog, query);
  if (candidates.length === 1) {
    return {
      kind: "pac-pr-merge",
      migrated: true,
      mutationHintsAllowed: false,
      consumer: candidates[0],
      reason: "exact-pac-consumer",
      valuesPrinted: false,
    };
  }
  if (candidates.length > 1) {
    return unknownAuthority("authority-ambiguous", `identity matches multiple PaC consumers: ${candidates.map((item) => item.consumerId).join(", ")}`, catalog.configRefs);
  }
  if (suppliedIdentity && query.consumerId !== undefined && query.consumerId !== null) {
    return unknownAuthority("authority-identity-mismatch", `consumer identity does not match ${query.consumerId}`, catalog.configRefs);
  }
  if (stableCandidates.length > 0) {
    return unknownAuthority(
      stableCandidates.length > 1 ? "authority-ambiguous" : "authority-identity-mismatch",
      `stable PaC identity conflicts with source details: ${stableCandidates.map((item) => item.consumerId).join(", ")}`,
      catalog.configRefs,
    );
  }
  const declaredMode = normalized(query.declaredSourceAuthorityMode);
  const declaredConfigRef = normalized(query.declaredConfigRef);
  if (declaredMode !== null && legacyAuthorityModes.has(declaredMode) && declaredConfigRef !== null) {
    return {
      kind: "legacy-manual",
      migrated: false,
      mutationHintsAllowed: true,
      consumer: null,
      reason: "explicit-legacy-source-authority",
      declaredSourceAuthorityMode: declaredMode,
      declaredConfigRef,
      valuesPrinted: false,
    };
  }
  return unknownAuthority(
    suppliedIdentity ? "authority-identity-mismatch" : "authority-not-declared",
    suppliedIdentity ? "no exact PaC consumer or explicit legacy authority matched" : "delivery authority identity is not declared",
    catalog.configRefs,
  );
}

function stableIdentityCandidates(catalog: CicdDeliveryAuthorityCatalog, query: CicdDeliveryAuthorityQuery): readonly PacDeliveryConsumerAuthority[] {
  const consumerId = normalized(query.consumerId);
  if (consumerId !== null) return catalog.consumers.filter((consumer) => same(consumer.consumerId, consumerId));
  const node = normalized(query.node);
  const lane = normalized(query.lane);
  if (node === null || lane === null) return [];
  return catalog.consumers.filter((consumer) => same(consumer.node, node) && same(consumer.lane, lane));
}

export function decideCicdDeliveryMutation(authority: CicdDeliveryAuthority, context: CicdDeliveryMutationContext): CicdDeliveryMutationDecision {
  if (authority.kind === "legacy-manual") return { allowed: true, authority };
  const next = authority.kind === "pac-pr-merge"
    ? pacReadOnlyNext(authority.consumer.node, authority.consumer.consumerId)
    : {
        status: context.statusCommand,
        fixManualRelease: pacManualReleaseFix(authority),
        valuesPrinted: false,
      };
  return {
    allowed: false,
    authority,
    result: {
      ok: false,
      command: context.command,
      mode: authority.kind === "pac-pr-merge" ? "pac-pr-merge-mutation-blocked" : "delivery-authority-unknown",
      mutation: false,
      requestedAction: context.action,
      node: context.node,
      lane: context.lane,
      reason: authority.kind === "pac-pr-merge"
        ? "该 lane 已迁移到 PaC 手动发布；GitHub PR merge 只更新 source authority，请使用 release plan 和 release trigger。"
        : "无法从 owning YAML 唯一确认 delivery authority；CLI 已 fail-closed。",
      deliveryAuthority: authority,
      next,
      valuesPrinted: false,
    },
  };
}

export function gitRepositoryIdentity(remoteUrl: string): string | null {
  const value = remoteUrl.trim();
  if (value.length === 0) return null;
  const scp = value.match(/^[^@\s]+@[^:\s]+:([^\s?#]+?)(?:\.git)?$/u);
  if (scp !== null) return normalizedRepositoryPath(scp[1]);
  try {
    const url = new URL(value);
    return normalizedRepositoryPath(url.pathname);
  } catch {
    return normalizedRepositoryPath(value);
  }
}

export function pacManualReleaseFix(authority: CicdDeliveryAuthority): Record<string, unknown> {
  const configRefs = authority.kind === "pac-pr-merge" ? authority.consumer.configRefs : authority.kind === "unknown" ? authority.configRefs : [authority.declaredConfigRef];
  return manualReleaseFix(configRefs, authority.kind === "unknown" ? "fix-delivery-authority" : "fix-manual-release");
}

export function pacReadOnlyNext(targetId: string, consumerId: string): Record<string, unknown> {
  return {
    status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId} --consumer ${consumerId}`,
    history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId} --consumer ${consumerId}`,
    fixManualRelease: manualReleaseFix([pacConfigPath, giteaConfigPath], "fix-manual-release"),
    valuesPrinted: false,
  };
}

export function pacNodeReadOnlyNext(targetId: string): Record<string, unknown> {
  return {
    status: `bun scripts/cli.ts cicd status --node ${targetId}`,
    history: `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${targetId} --limit 10`,
    fixManualRelease: manualReleaseFix([pacConfigPath, giteaConfigPath], "fix-manual-release"),
    valuesPrinted: false,
  };
}

function manualReleaseFix(configRefs: readonly string[], code: "fix-manual-release" | "fix-delivery-authority"): Record<string, unknown> {
  return {
    code,
    reference: PAC_AUTOMATIC_DELIVERY_REFERENCE,
    configRefs,
    instruction: "修复 owning YAML、planner、controller 或源码，重新执行 release plan；只有范围准确时才执行 release trigger --confirm。",
    mutation: false,
    valuesPrinted: false,
  };
}

function normalizedRepositoryPath(value: string): string | null {
  const result = value.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "").trim();
  if (!/^[^/\s]+\/[^/\s]+$/u.test(result)) return null;
  return result;
}

function matchesQuery(consumer: PacDeliveryConsumerAuthority, query: CicdDeliveryAuthorityQuery): boolean {
  return matches(consumer.consumerId, query.consumerId)
    && matches(consumer.node, query.node)
    && matches(consumer.lane, query.lane)
    && matches(consumer.sourceRepository, query.sourceRepository)
    && matches(consumer.sourceBranch, query.sourceBranch);
}

function matches(actual: string, expected: string | null | undefined): boolean {
  const value = normalized(expected);
  return value === null || same(actual, value);
}

function same(left: string, right: string): boolean {
  return left.toLowerCase() === right.toLowerCase();
}

function normalized(value: string | null | undefined): string | null {
  if (typeof value !== "string") return null;
  const result = value.trim();
  return result.length === 0 ? null : result;
}

function unknownAuthority(reason: Extract<CicdDeliveryAuthority, { kind: "unknown" }>["reason"], detail: string, configRefs: readonly string[]): CicdDeliveryAuthority {
  return { kind: "unknown", migrated: false, mutationHintsAllowed: false, consumer: null, reason, detail, configRefs, valuesPrinted: false };
}

function composedYaml(path: string, kind: string): Record<string, unknown> {
  return materializeYamlComposition(readYamlRecord<Record<string, unknown>>(rootPath(...path.split("/")), kind), { label: path }).value;
}

function record(value: unknown, path: string): Record<string, unknown> {
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} must be an object`);
  return value as Record<string, unknown>;
}

function recordArray(value: unknown, path: string): Record<string, unknown>[] {
  if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
  return value.map((item, index) => record(item, `${path}[${index}]`));
}

function requiredString(value: unknown, path: string): string {
  if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path} must be a non-empty string`);
  return value.trim();
}

function exactlyOne<T>(items: T[], path: string): T {
  if (items.length !== 1) throw new Error(`${path} must resolve exactly one row; got ${items.length}`);
  return items[0];
}
