import type { IMcpServerRepository, ISecretRepository } from '../../repositories/interfaces.js';
import type { IProjectRepository } from '../../repositories/project.repository.js';
import type { IUserRepository } from '../../repositories/user.repository.js';
import type { IGroupRepository } from '../../repositories/group.repository.js';
import type { IRbacDefinitionRepository } from '../../repositories/rbac-definition.repository.js';
import type { IPromptRepository } from '../../repositories/prompt.repository.js';
import type { ITemplateRepository } from '../../repositories/template.repository.js';
import type { RbacRoleBinding } from '../../validation/rbac-definition.schema.js';
import type { SecretService } from '../secret.service.js';
import { decrypt } from './crypto.js';
import type { BackupBundle } from './backup-service.js';

export type ConflictStrategy = 'skip' | 'overwrite' | 'fail';

export interface RestoreOptions {
  password?: string;
  conflictStrategy?: ConflictStrategy;
}

export interface RestoreResult {
  serversCreated: number;
  serversSkipped: number;
  secretsCreated: number;
  secretsSkipped: number;
  projectsCreated: number;
  projectsSkipped: number;
  usersCreated: number;
  usersSkipped: number;
  groupsCreated: number;
  groupsSkipped: number;
  rbacCreated: number;
  rbacSkipped: number;
  promptsCreated: number;
  promptsSkipped: number;
  templatesCreated: number;
  templatesSkipped: number;
  errors: string[];
}

export class RestoreService {
  constructor(
    private serverRepo: IMcpServerRepository,
    private projectRepo: IProjectRepository,
    private secretRepo: ISecretRepository,
    private secretService: SecretService,
    private userRepo?: IUserRepository,
    private groupRepo?: IGroupRepository,
    private rbacRepo?: IRbacDefinitionRepository,
    private promptRepo?: IPromptRepository,
    private templateRepo?: ITemplateRepository,
  ) {}

  validateBundle(bundle: unknown): bundle is BackupBundle {
    if (typeof bundle !== 'object' || bundle === null) return false;
    const b = bundle as Record<string, unknown>;
    return (
      typeof b['version'] === 'string' &&
      Array.isArray(b['servers']) &&
      Array.isArray(b['secrets']) &&
      Array.isArray(b['projects'])
    );
    // users, groups, rbacBindings are optional for backwards compatibility
  }

  async restore(bundle: BackupBundle, options?: RestoreOptions): Promise<RestoreResult> {
    const strategy = options?.conflictStrategy ?? 'skip';
    const result: RestoreResult = {
      serversCreated: 0,
      serversSkipped: 0,
      secretsCreated: 0,
      secretsSkipped: 0,
      projectsCreated: 0,
      projectsSkipped: 0,
      usersCreated: 0,
      usersSkipped: 0,
      groupsCreated: 0,
      groupsSkipped: 0,
      rbacCreated: 0,
      rbacSkipped: 0,
      promptsCreated: 0,
      promptsSkipped: 0,
      templatesCreated: 0,
      templatesSkipped: 0,
      errors: [],
    };

    // Decrypt secrets if encrypted
    let decryptedSecrets: Record<string, string> = {};
    if (bundle.encrypted && bundle.encryptedSecrets) {
      if (!options?.password) {
        result.errors.push('Backup is encrypted but no password provided');
        return result;
      }
      try {
        decryptedSecrets = JSON.parse(decrypt(bundle.encryptedSecrets, options.password)) as Record<string, string>;
      } catch {
        result.errors.push('Failed to decrypt backup - incorrect password or corrupted data');
        return result;
      }
    }

    // Restore encrypted values into secret data
    for (const secret of bundle.secrets) {
      for (const [key, value] of Object.entries(secret.data)) {
        if (typeof value === 'string' && value.startsWith('__ENCRYPTED:') && value.endsWith('__')) {
          const secretKey = value.slice(12, -2);
          const decrypted = decryptedSecrets[secretKey];
          if (decrypted !== undefined) {
            secret.data[key] = decrypted;
          }
        }
      }
    }

    // Restore order: secrets → servers → users → groups → projects → rbacBindings

    // Restore secrets
    for (const secret of bundle.secrets) {
      try {
        const existing = await this.secretRepo.findByName(secret.name);
        if (existing) {
          if (strategy === 'fail') {
            result.errors.push(`Secret "${secret.name}" already exists`);
            return result;
          }
          if (strategy === 'skip') {
            result.secretsSkipped++;
            continue;
          }
          // overwrite — route through SecretService so backend dispatch applies.
          await this.secretService.update(existing.id, { data: secret.data });
          result.secretsCreated++;
          continue;
        }

        await this.secretService.create({ name: secret.name, data: secret.data });
        result.secretsCreated++;
      } catch (err) {
        result.errors.push(`Failed to restore secret "${secret.name}": ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    // Restore servers
    for (const server of bundle.servers) {
      try {
        const existing = await this.serverRepo.findByName(server.name);
        if (existing) {
          if (strategy === 'fail') {
            result.errors.push(`Server "${server.name}" already exists`);
            return result;
          }
          if (strategy === 'skip') {
            result.serversSkipped++;
            continue;
          }
          // overwrite
          const updateData: Parameters<IMcpServerRepository['update']>[1] = {
            description: server.description,
            transport: server.transport as 'STDIO' | 'SSE' | 'STREAMABLE_HTTP',
          };
          if (server.packageName) updateData.packageName = server.packageName;
          if (server.dockerImage) updateData.dockerImage = server.dockerImage;
          if (server.repositoryUrl) updateData.repositoryUrl = server.repositoryUrl;
          await this.serverRepo.update(existing.id, updateData);
          result.serversCreated++;
          continue;
        }

        const createData: Parameters<IMcpServerRepository['create']>[0] = {
          name: server.name,
          description: server.description,
          transport: server.transport as 'STDIO' | 'SSE' | 'STREAMABLE_HTTP',
          replicas: server.replicas ?? 1,
          env: (server.env ?? []) as Array<{ name: string; value?: string; valueFrom?: { secretRef: { name: string; key: string } } }>,
        };
        if (server.packageName) createData.packageName = server.packageName;
        if (server.runtime) createData.runtime = server.runtime;
        if (server.dockerImage) createData.dockerImage = server.dockerImage;
        if (server.repositoryUrl) createData.repositoryUrl = server.repositoryUrl;
        if (server.externalUrl) createData.externalUrl = server.externalUrl;
        if (server.command) createData.command = server.command as string[];
        if (server.containerPort) createData.containerPort = server.containerPort;
        if (server.healthCheck) createData.healthCheck = server.healthCheck as Parameters<IMcpServerRepository['create']>[0]['healthCheck'];
        await this.serverRepo.create(createData);
        result.serversCreated++;
      } catch (err) {
        result.errors.push(`Failed to restore server "${server.name}": ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    // Restore users
    if (bundle.users && this.userRepo) {
      for (const user of bundle.users) {
        try {
          const existing = await this.userRepo.findByEmail(user.email);
          if (existing) {
            if (strategy === 'fail') {
              result.errors.push(`User "${user.email}" already exists`);
              return result;
            }
            result.usersSkipped++;
            continue;
          }

          // Create with placeholder passwordHash — user must reset password
          const createData: { email: string; passwordHash: string; name?: string; role?: string } = {
            email: user.email,
            passwordHash: '__RESTORED_MUST_RESET__',
            role: user.role,
          };
          if (user.name !== null) createData.name = user.name;
          await this.userRepo.create(createData);
          result.usersCreated++;
        } catch (err) {
          result.errors.push(`Failed to restore user "${user.email}": ${err instanceof Error ? err.message : String(err)}`);
        }
      }
    }

    // Restore groups
    if (bundle.groups && this.groupRepo && this.userRepo) {
      for (const group of bundle.groups) {
        try {
          const existing = await this.groupRepo.findByName(group.name);
          if (existing) {
            if (strategy === 'fail') {
              result.errors.push(`Group "${group.name}" already exists`);
              return result;
            }
            if (strategy === 'skip') {
              result.groupsSkipped++;
              continue;
            }
            // overwrite: update description and re-set members
            await this.groupRepo.update(existing.id, { description: group.description });
            if (group.memberEmails.length > 0) {
              const memberIds = await this.resolveUserEmails(group.memberEmails);
              await this.groupRepo.setMembers(existing.id, memberIds);
            }
            result.groupsCreated++;
            continue;
          }

          const created = await this.groupRepo.create({
            name: group.name,
            description: group.description,
          });
          if (group.memberEmails.length > 0) {
            const memberIds = await this.resolveUserEmails(group.memberEmails);
            await this.groupRepo.setMembers(created.id, memberIds);
          }
          result.groupsCreated++;
        } catch (err) {
          result.errors.push(`Failed to restore group "${group.name}": ${err instanceof Error ? err.message : String(err)}`);
        }
      }
    }

    // Restore projects (enriched)
    for (const project of bundle.projects) {
      try {
        const existing = await this.projectRepo.findByName(project.name);
        if (existing) {
          if (strategy === 'fail') {
            result.errors.push(`Project "${project.name}" already exists`);
            return result;
          }
          if (strategy === 'skip') {
            result.projectsSkipped++;
            continue;
          }
          // overwrite
          const updateData: Record<string, unknown> = { description: project.description };
          if (project.proxyModel) updateData['proxyModel'] = project.proxyModel;
          if (project.llmProvider !== undefined) updateData['llmProvider'] = project.llmProvider;
          if (project.llmModel !== undefined) updateData['llmModel'] = project.llmModel;
          await this.projectRepo.update(existing.id, updateData);

          // Re-link servers
          if (project.serverNames && project.serverNames.length > 0) {
            const serverIds = await this.resolveServerNames(project.serverNames);
            await this.projectRepo.setServers(existing.id, serverIds);
          }

          result.projectsCreated++;
          continue;
        }

        // Resolve a valid owner — prefer system user, fall back to first user
        let ownerId = '';
        if (this.userRepo) {
          const allUsers = await this.userRepo.findAll();
          for (const u of allUsers) {
            if (u.email === 'system@mcpctl.local') { ownerId = u.id; break; }
            if (!ownerId) ownerId = u.id;
          }
        }

        const projectCreateData: { name: string; description: string; ownerId: string; proxyModel?: string; llmProvider?: string; llmModel?: string } = {
          name: project.name,
          description: project.description,
          ownerId,
        };
        if (project.proxyModel) projectCreateData.proxyModel = project.proxyModel;
        if (project.llmProvider != null) projectCreateData.llmProvider = project.llmProvider;
        if (project.llmModel != null) projectCreateData.llmModel = project.llmModel;
        const created = await this.projectRepo.create(projectCreateData);

        // Link servers
        if (project.serverNames && project.serverNames.length > 0) {
          const serverIds = await this.resolveServerNames(project.serverNames);
          await this.projectRepo.setServers(created.id, serverIds);
        }

        result.projectsCreated++;
      } catch (err) {
        result.errors.push(`Failed to restore project "${project.name}": ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    // Restore RBAC bindings
    if (bundle.rbacBindings && this.rbacRepo) {
      for (const rbac of bundle.rbacBindings) {
        try {
          const existing = await this.rbacRepo.findByName(rbac.name);
          if (existing) {
            if (strategy === 'fail') {
              result.errors.push(`RBAC binding "${rbac.name}" already exists`);
              return result;
            }
            if (strategy === 'skip') {
              result.rbacSkipped++;
              continue;
            }
            // overwrite
            await this.rbacRepo.update(existing.id, {
              subjects: rbac.subjects as Array<{ kind: 'User' | 'Group'; name: string }>,
              roleBindings: rbac.roleBindings as RbacRoleBinding[],
            });
            result.rbacCreated++;
            continue;
          }

          await this.rbacRepo.create({
            name: rbac.name,
            subjects: rbac.subjects as Array<{ kind: 'User' | 'Group'; name: string }>,
            roleBindings: rbac.roleBindings as RbacRoleBinding[],
          });
          result.rbacCreated++;
        } catch (err) {
          result.errors.push(`Failed to restore RBAC binding "${rbac.name}": ${err instanceof Error ? err.message : String(err)}`);
        }
      }
    }

    // Restore prompts (after projects, so projectId can be resolved)
    if (bundle.prompts && this.promptRepo) {
      for (const prompt of bundle.prompts) {
        try {
          // Resolve project by name
          let projectId: string | undefined;
          if (prompt.projectName) {
            const project = await this.projectRepo.findByName(prompt.projectName);
            if (project) projectId = project.id;
          }

          const existing = await this.promptRepo.findByNameAndProject(prompt.name, projectId ?? null);
          if (existing) {
            if (strategy === 'fail') {
              result.errors.push(`Prompt "${prompt.name}" already exists`);
              return result;
            }
            if (strategy === 'skip') {
              result.promptsSkipped++;
              continue;
            }
            // overwrite
            const updateData: { content: string; priority: number; summary?: string } = {
              content: prompt.content,
              priority: prompt.priority,
            };
            if (prompt.summary) updateData.summary = prompt.summary;
            await this.promptRepo.update(existing.id, updateData);
            result.promptsCreated++;
            continue;
          }

          const createData: { name: string; content: string; projectId?: string; priority?: number; linkTarget?: string } = {
            name: prompt.name,
            content: prompt.content,
          };
          if (projectId) createData.projectId = projectId;
          if (prompt.priority !== 5) createData.priority = prompt.priority;
          if (prompt.linkTarget) createData.linkTarget = prompt.linkTarget;
          await this.promptRepo.create(createData);
          result.promptsCreated++;
        } catch (err) {
          result.errors.push(`Failed to restore prompt "${prompt.name}": ${err instanceof Error ? err.message : String(err)}`);
        }
      }
    }

    // Restore templates
    if (bundle.templates && this.templateRepo) {
      for (const tmpl of bundle.templates) {
        try {
          const existing = await this.templateRepo.findByName(tmpl.name);
          if (existing) {
            if (strategy === 'skip') {
              result.templatesSkipped++;
              continue;
            }
            // overwrite or fail handled by upsert
            result.templatesSkipped++;
            continue;
          }

          const tmplData: Record<string, unknown> = {
            name: tmpl.name,
            description: tmpl.description,
            transport: tmpl.transport as 'STDIO' | 'SSE' | 'STREAMABLE_HTTP',
          };
          if (tmpl.packageName) tmplData.packageName = tmpl.packageName;
          if (tmpl.dockerImage) tmplData.dockerImage = tmpl.dockerImage;
          if (tmpl.command) tmplData.command = tmpl.command;
          if (tmpl.containerPort) tmplData.containerPort = tmpl.containerPort;
          if (tmpl.env) tmplData.env = tmpl.env;
          if (tmpl.healthCheck) tmplData.healthCheck = tmpl.healthCheck;
          await this.templateRepo.create(tmplData as Parameters<typeof this.templateRepo.create>[0]);
          result.templatesCreated++;
        } catch (err) {
          result.errors.push(`Failed to restore template "${tmpl.name}": ${err instanceof Error ? err.message : String(err)}`);
        }
      }
    }

    return result;
  }

  /** Resolve email addresses to user IDs via the user repository. */
  private async resolveUserEmails(emails: string[]): Promise<string[]> {
    const ids: string[] = [];
    for (const email of emails) {
      const user = await this.userRepo!.findByEmail(email);
      if (user) ids.push(user.id);
    }
    return ids;
  }

  /** Resolve server names to server IDs via the server repository. */
  private async resolveServerNames(names: string[]): Promise<string[]> {
    const ids: string[] = [];
    for (const name of names) {
      const server = await this.serverRepo.findByName(name);
      if (server) ids.push(server.id);
    }
    return ids;
  }

}
