import { randomUUID } from 'crypto';
import path from 'path';
import { NodeRegistry } from '../NodeRegistry';
import { MANAGED_ROOT_NAME } from './managedPaths';
import { encodeGitOpsJson } from './json';
import { materializationFingerprint } from './fingerprint';
import { parseLegacyRepoUrl, parseStorableRepoUrl, secretFreeRepoUrl, secretFreeRepoUrlFromStorable, serializeRepoIdentity, serializeRepoIdentityFromStorable, type RepoIdentity } from './repoIdentity';
import type { RefKind } from '../git/types';
import type {
  GitOpsApplicationRow,
  GitOpsCreateCheckpointRow,
  GitOpsGenerationRow,
} from './types';

/** The material source configuration a Direct application is bound to. */
export type DirectSourceConfig = {
  repoUrl: string;
  branch: string;
  composePaths: readonly string[];
  contextDir: string | null;
  syncEnv: boolean;
  envPath: string | null;
};

export type DirectSourceIdentity = {
  /** Secret-free `https://host/pathname`, safe to persist and to project. */
  repoUrl: string;
  identity: RepoIdentity;
  fingerprint: string;
};

export class GitOpsIdentityError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'GitOpsIdentityError';
  }
}

/**
 * Derive the storable identity and materialization fingerprint for a source.
 *
 * The fingerprint is what later decides whether a staged candidate still
 * matches the configuration it was built from, so it is computed from the same
 * secret-free identity that gets persisted, never from the raw operational URL.
 */
export function directSourceIdentity(config: DirectSourceConfig): DirectSourceIdentity {
  const parsed = parseStorableRepoUrl(config.repoUrl);
  if (!parsed.ok) throw new GitOpsIdentityError(`repository URL is not storable: ${parsed.reason}`);
  const identity = serializeRepoIdentityFromStorable(parsed);
  const repoUrl = secretFreeRepoUrlFromStorable(parsed);
  const fingerprint = materializationFingerprint({
    repoIdentity: identity,
    configuredRef: config.branch,
    composePaths: config.composePaths,
    contextDir: config.contextDir,
    syncEnv: config.syncEnv,
    envPath: config.envPath,
  });
  return { repoUrl, identity, fingerprint };
}

/**
 * The same derivation, for URLs that predate strict ingress.
 *
 * Migration is its only caller. A legacy operational row may still carry
 * userinfo or a query string that fetch needs, so the storable identity strips
 * them instead of refusing the stack; the strict helper above stays the gate
 * for every path a user can drive.
 */
export function migrationDirectSourceIdentity(config: DirectSourceConfig): DirectSourceIdentity {
  const parsed = parseLegacyRepoUrl(config.repoUrl);
  if (!parsed.ok) throw new GitOpsIdentityError(`repository URL is not storable: ${parsed.reason}`);
  return directSourceIdentityFromUrl(config, parsed.url);
}

function directSourceIdentityFromUrl(config: DirectSourceConfig, url: URL): DirectSourceIdentity {
  const identity = serializeRepoIdentity(url);
  const material = {
    repoIdentity: identity,
    configuredRef: config.branch,
    composePaths: config.composePaths,
    contextDir: config.contextDir,
    syncEnv: config.syncEnv,
    envPath: config.envPath,
  };
  return {
    repoUrl: secretFreeRepoUrl(identity),
    identity,
    fingerprint: materializationFingerprint(material),
  };
}

/** Absolute managed root for one stack on the local node. */
export function stackManagedRoot(stackName: string): string {
  const dataDir = process.env.DATA_DIR || path.join(process.cwd(), 'data');
  const nodeId = NodeRegistry.getInstance().getDefaultNodeId();
  return path.join(dataDir, MANAGED_ROOT_NAME, String(nodeId), stackName);
}

export function newGitOpsId(): string {
  return randomUUID();
}

/**
 * Build a Direct application row.
 *
 * `creating` is for create-from-Git, where the stack does not exist yet and the
 * checkpoint decides what happens if the process dies. `active` is for linking
 * a stack that already exists: there is nothing to recover, so it is live from
 * the moment the source row commits.
 */
export function buildDirectApplicationRow(args: {
  id: string;
  stackName: string;
  config: DirectSourceConfig;
  identity: DirectSourceIdentity;
  lifecycleStatus: 'creating' | 'active';
  at: number;
}): GitOpsApplicationRow {
  return {
    id: args.id,
    lifecycle_key: `direct:${args.stackName}`,
    lifecycle_status: args.lifecycleStatus,
    target_mode: 'direct',
    stack_name: args.stackName,
    blueprint_id: null,
    configured_repo_url: args.identity.repoUrl,
    repo_identity_json: encodeGitOpsJson(args.identity.identity),
    configured_ref: args.config.branch,
    compose_paths_json: encodeGitOpsJson([...args.config.composePaths]),
    context_dir: args.config.contextDir,
    sync_env: args.config.syncEnv ? 1 : 0,
    env_path: args.config.syncEnv ? args.config.envPath : null,
    materialization_fingerprint: args.identity.fingerprint,
    desired_commit_sha: null,
    fetched_commit_sha: null,
    fetched_resolved_ref_kind: null,
    candidate_generation_id: null,
    accepted_generation_id: null,
    candidate_plan_blocked: 0,
    review_required: 0,
    artifact_set_id: null,
    latest_artifact_set_id: null,
    intent_revision_id: null,
    rollout_candidate_id: null,
    rollout_generation_id: null,
    source_acceptance_ref: null,
    placement_approval_ref: null,
    rollout_authorization_ref: null,
    legacy_combined_approval_ref: null,
    preflight_fingerprint: null,
    latest_operation_id: null,
    active_operation_id: null,
    active_operation_stage: null,
    active_operation_at: null,
    active_generation_id: null,
    pause_at: null,
    pause_reason: null,
    source_suspended_reason: null,
    partial_json: null,
    failure_stage: null,
    failure_class: null,
    failure_at: null,
    retry_at: null,
    retry_count: 0,
    suspended_at: null,
    recovery_ref: null,
    recovery_phase: null,
    interruption_stage: null,
    interruption_at: null,
    interruption_operation_id: null,
    interruption_generation_id: null,
    evidence_fresh_at: null,
    evidence_limitations_json: null,
    created_at: args.at,
    updated_at: args.at,
  };
}

export function buildGenerationRow(args: {
  id: string;
  applicationId: string;
  commitSha: string;
  identity: DirectSourceIdentity;
  configuredRef: string;
  resolvedRefKind: RefKind;
  candidateRelPath: string;
  appliedRelPath: string;
  manifestVersion: number;
  expectedInvocation: unknown;
  changePlanFingerprint: string | null;
  operationId: string;
  trigger: string;
  actor: string | null;
  at: number;
  /** A blocked change plan is recorded, but such a generation can never apply. */
  planBlocked?: boolean;
}): GitOpsGenerationRow {
  return {
    id: args.id,
    application_id: args.applicationId,
    commit_sha: args.commitSha,
    repo_url: args.identity.repoUrl,
    configured_ref: args.configuredRef,
    resolved_ref_kind: args.resolvedRefKind,
    repo_identity_json: encodeGitOpsJson(args.identity.identity),
    manifest_version: args.manifestVersion,
    candidate_dir: args.candidateRelPath,
    applied_dir: args.appliedRelPath,
    expected_invocation_json: encodeGitOpsJson(args.expectedInvocation),
    materialization_fingerprint: args.identity.fingerprint,
    validation_ok: 1,
    plan_blocked: args.planBlocked ? 1 : 0,
    change_plan_fingerprint: args.changePlanFingerprint,
    operation_id: args.operationId,
    trigger: args.trigger,
    actor: args.actor,
    previous_generation_id: null,
    redacted_limitations_json: '[]',
    created_at: args.at,
  };
}

export function buildCreateCheckpointRow(args: {
  applicationId: string;
  stackName: string;
  operationId: string;
  config: DirectSourceConfig;
  identity: DirectSourceIdentity;
  authType: string;
  encryptedToken: string | null;
  encryptedDeployKey?: string | null;
  sshKnownHostsEntry?: string | null;
  sshHostKeyFingerprint?: string | null;
  encryptedCaBundle?: string | null;
  autoApplyOnWebhook: boolean;
  autoDeployOnApply: boolean;
  commitSha: string;
  createdManagedRoot: boolean;
  at: number;
}): GitOpsCreateCheckpointRow {
  return {
    application_id: args.applicationId,
    stack_name: args.stackName,
    phase: 'pre_stack',
    generation_id: null,
    operation_id: args.operationId,
    // Operational URL for fetch compatibility during create. Copies into
    // generations and history always go through the secret-free identity.
    repo_url: args.config.repoUrl,
    branch: args.config.branch,
    compose_path: args.config.composePaths[0] ?? '',
    compose_paths_json: encodeGitOpsJson([...args.config.composePaths]),
    context_dir: args.config.contextDir,
    sync_env: args.config.syncEnv ? 1 : 0,
    env_path: args.config.syncEnv ? args.config.envPath : null,
    auth_type: args.authType,
    encrypted_token: args.encryptedToken,
    encrypted_deploy_key: args.encryptedDeployKey ?? null,
    ssh_known_hosts_entry: args.sshKnownHostsEntry ?? null,
    ssh_host_key_fingerprint: args.sshHostKeyFingerprint ?? null,
    encrypted_ca_bundle: args.encryptedCaBundle ?? null,
    auto_apply_on_webhook: args.autoApplyOnWebhook ? 1 : 0,
    auto_deploy_on_apply: args.autoDeployOnApply ? 1 : 0,
    commit_sha: args.commitSha,
    applied_spec_json: null,
    created_managed_root: args.createdManagedRoot ? 1 : 0,
    created_at: args.at,
    updated_at: args.at,
  };
}
