import {
  Injectable,
  BadRequestException,
  ConflictException,
  Logger,
  OnModuleInit,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { type EntityManager, Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { TenantEntity } from '../../entities/tenant.entity';
import { UserEntity } from '../../entities/user.entity';
import { RoleEntity } from '../../entities/role.entity';
import {
  RolePermissionEntity,
  AccessLevel,
  ModuleName,
  MASTER_MODULES_VIEW_DEFAULT,
} from '../../entities/role-permission.entity';
import { TenantSettingsEntity } from '../../entities/tenant-settings.entity';
import { ServiceTypeEntity } from '../../entities/service-type.entity';
import { TaxTypeEntity } from '../../entities/tax-type.entity';
import { VehicleTypeEntity } from '../../entities/vehicle-type.entity';
import { CurrencyEntity, CurrencyFormat } from '../../entities/currency.entity';
import { OnboardTenantDto } from './dto/onboard-tenant.dto';
import { RedisService } from '../../redis/redis.service';
import { RoleHierarchyEntity } from '../../entities/role-hierarchy.entity';
import { DELETED_MEDIA_ROOT } from '../../common/media/media.constants';

const SEED_SERVICE_TYPES = [
  { name: 'Hotels', code: 'hotels', description: 'Hotel accommodation services' },
  { name: 'Transport', code: 'transport', description: 'Transportation and transfer services' },
  { name: 'Activities', code: 'activities', description: 'Tours, excursions, and activities' },
];

const SEED_CURRENCIES = [
  {
    name: 'US Dollar',
    code: 'USD',
    symbol: '$',
    rounding: 1,
    format: CurrencyFormat.INTERNATIONAL,
  },
  { name: 'Euro', code: 'EUR', symbol: '€', rounding: 1, format: CurrencyFormat.INTERNATIONAL },
  {
    name: 'British Pound',
    code: 'GBP',
    symbol: '£',
    rounding: 1,
    format: CurrencyFormat.INTERNATIONAL,
  },
  { name: 'Indian Rupee', code: 'INR', symbol: '₹', rounding: 1, format: CurrencyFormat.INDIAN },
  {
    name: 'UAE Dirham',
    code: 'AED',
    symbol: 'د.إ',
    rounding: 1,
    format: CurrencyFormat.INTERNATIONAL,
  },
];

const SEED_VEHICLE_TYPES = [
  { name: 'Bus' },
  { name: 'Coaster' },
  { name: 'Limousine' },
  { name: 'Mini Bus' },
  { name: 'Sedan' },
  { name: 'SUV' },
  { name: 'Van' },
];

const SEED_TAX_TYPES = [
  { name: 'GST', display_name: 'Goods and Services Tax (18%)', default_value: 18 },
  { name: 'VAT', display_name: 'Value Added Tax (20%)', default_value: 20 },
  { name: 'Service Tax', display_name: 'Service Tax (15%)', default_value: 15 },
  { name: 'Tourism Tax', display_name: 'Tourism Development Tax (5%)', default_value: 5 },
];

/**
 * Predefined roles seeded per scope doc (User Management → Role Management):
 * - Admin (full access to all modules)
 * - Operations Manager
 * - Trip Coordinator
 * - Sales Head (manages the sales team — added 2026-05-08 for the
 *   Role-Based Dashboards feature; see scope.md "Role-Based Dashboards")
 * - Sales Team
 * - Guest (read-only)
 *
 * Per CLAUDE.md:
 * - No Super Admin in Phase 1 — Admin is highest role, seeded during tenant onboarding
 * - Roles fully dynamic — these are starting templates, admin can modify
 *
 * NOTE: existing tenants are migrated to include Sales Head via the
 * idempotent migration `1778100000000-SalesHeadRole.ts`. New tenants
 * pick it up automatically through `onboard()` below.
 */
const SEED_ROLES: Array<{
  name: string;
  description: string;
  defaultAccess: AccessLevel;
  /** Per-module overrides applied on top of defaultAccess. Use this
   *  when a role needs tighter or looser access than its flat default
   *  for a specific module. Example: Sales Team defaults to VIEW but
   *  must be NONE for MARKUP so selling-price config is hidden. */
  moduleOverrides?: Partial<Record<ModuleName, AccessLevel>>;
}> = [
  {
    name: 'Admin',
    description: 'Full access to all modules',
    defaultAccess: AccessLevel.DELETE,
  },
  {
    name: 'Operations Manager',
    description: 'Manage bookings, drivers, vehicles, and operations',
    defaultAccess: AccessLevel.EDIT,
    moduleOverrides: {
      // Floor-price visibility is finance/admin territory. Ops can
      // edit selling prices but should NOT see floor (minimum) prices
      // — that would expose the company's bottom margin.
      [ModuleName.TRANSPORT_FLOOR_PRICING]: AccessLevel.NONE,
      [ModuleName.ACTIVITY_FLOOR_PRICING]: AccessLevel.NONE,
      // Settlements are Admin + Accountant only per stakeholder spec
      // (see migration 1780100000000-SettlementAdminAccountantOnly).
      // Without this override, the on-boot backfill kept re-inserting
      // Ops Manager's row at EDIT and undoing the migration revoke.
      [ModuleName.PAYMENT_SETTLEMENT]: AccessLevel.NONE,
    },
  },
  {
    name: 'Trip Coordinator',
    description: 'Handle queries, quotations, and trip logistics',
    defaultAccess: AccessLevel.CREATE,
    moduleOverrides: {
      // Can see pending approvals (context) but not decide.
      [ModuleName.BOOKING_APPROVAL]: AccessLevel.VIEW,
      // Same floor-price restriction as Operations Manager.
      [ModuleName.TRANSPORT_FLOOR_PRICING]: AccessLevel.NONE,
      [ModuleName.ACTIVITY_FLOOR_PRICING]: AccessLevel.NONE,
    },
  },
  {
    name: 'Sales Head',
    description:
      'Lead the sales team — triage queries, approve quotes/discounts, monitor team performance',
    defaultAccess: AccessLevel.VIEW,
    moduleOverrides: {
      // Triage / reassign / edit any team query and its attached quotes.
      [ModuleName.QUERY]: AccessLevel.EDIT,
      // Decide booking change requests (discount overrides, zero-price
      // saves). Higher than Sales Team (NONE) and Trip Coordinator (VIEW).
      [ModuleName.BOOKING_APPROVAL]: AccessLevel.EDIT,
      // Sales Head owns Selling Price configuration in Transport
      // Pricing (Selling row). Floor row is hidden by the floor-
      // pricing override below.
      [ModuleName.TRANSPORT_PRICING]: AccessLevel.EDIT,
      [ModuleName.ACTIVITY_PRICING]: AccessLevel.EDIT,
      // Floor margins are not for sales — even at lead level.
      [ModuleName.TRANSPORT_FLOOR_PRICING]: AccessLevel.NONE,
      [ModuleName.ACTIVITY_FLOOR_PRICING]: AccessLevel.NONE,
      // Settlements are Admin + Accountant only per spec — same
      // contract as Operations Manager above. Without this override
      // the on-boot backfill re-inserted Sales Head's VIEW row.
      [ModuleName.PAYMENT_SETTLEMENT]: AccessLevel.NONE,
      // Supplier rollup is ops territory — sales stays off by default.
      [ModuleName.SUPPLIER_COLLECTION]: AccessLevel.NONE,
      // SETTINGS at default VIEW covers Users/Roles/Audit Log read-only;
      // creating/deleting users or roles still requires EDIT (Admin).
    },
  },
  {
    name: 'Sales Team',
    description: 'Manage queries and quotations',
    defaultAccess: AccessLevel.VIEW,
    moduleOverrides: {
      // Sales submits change requests but doesn't decide them.
      [ModuleName.BOOKING_APPROVAL]: AccessLevel.NONE,
      [ModuleName.TRANSPORT_FLOOR_PRICING]: AccessLevel.NONE,
      [ModuleName.ACTIVITY_FLOOR_PRICING]: AccessLevel.NONE,
      [ModuleName.SUPPLIER_COLLECTION]: AccessLevel.NONE,
    },
  },
  // Movement-Dashboard split roles (2026-05-13). Both see everything;
  // each edits only its own axis on the planner. Admin + Operations
  // Manager retain full EDIT on both via defaultAccess in their seed.
  {
    name: 'Transport Manager',
    description:
      'Edit transport assignments on the Movement Dashboard (driver / vehicle / supplier on transport rows)',
    defaultAccess: AccessLevel.VIEW,
    moduleOverrides: {
      [ModuleName.TRANSPORT_ASSIGNMENT]: AccessLevel.EDIT,
      // Explicit NONE so a future module added with defaultAccess=VIEW
      // doesn't accidentally let a Transport Manager edit activities.
      [ModuleName.ACTIVITY_ASSIGNMENT]: AccessLevel.NONE,
    },
  },
  {
    name: 'Activity Manager',
    description:
      'Edit activity assignments on the Movement Dashboard (supplier / pricing / status on activity rows)',
    defaultAccess: AccessLevel.VIEW,
    moduleOverrides: {
      [ModuleName.ACTIVITY_ASSIGNMENT]: AccessLevel.EDIT,
      [ModuleName.TRANSPORT_ASSIGNMENT]: AccessLevel.NONE,
    },
  },
  // Accountant role (2026-05-13) — finance-focused viewer. Seeded
  // because the client mentioned an Accountant will need to view
  // tax percentages going forward (client policy on tax % visibility
  // restricts the rate display to Admin + Accountant; the FE hook
  // `useCanSeeTaxPercent` already includes Accountant in its allow
  // list, so seeding the role here makes the policy enforceable).
  // Phase-1 default: VIEW everything. EDIT on Payment Settlement
  // arrives when §12 ships (the module isn't built yet).
  {
    name: 'Accountant',
    description: 'View pricing, taxes, and settlements; ready for Phase-1.5 finance workflows',
    defaultAccess: AccessLevel.VIEW,
    moduleOverrides: {
      // Accountant owns Floor Pricing (margin defence) on both
      // Transport and Activity pricing pages — finance contract.
      [ModuleName.TRANSPORT_FLOOR_PRICING]: AccessLevel.EDIT,
      [ModuleName.ACTIVITY_FLOOR_PRICING]: AccessLevel.EDIT,
      // Run the Settle / Record-Payment / Notes flows in Phase H. See
      // migration 1780100000000-SettlementAdminAccountantOnly.
      [ModuleName.PAYMENT_SETTLEMENT]: AccessLevel.EDIT,
    },
  },
];

/**
 * TenantService — tenant onboarding with role + admin user seeding.
 *
 * ⚠️ NOTE: This service does NOT use TenantAwareRepository because
 * it creates the tenant itself and operates across tenant boundaries
 * during onboarding. All queries here explicitly include tenant_id.
 *
 * ⚠️ CLAUDE.md caution: Raw queries / cross-tenant ops → manually add tenant_id
 */
@Injectable()
export class TenantService implements OnModuleInit {
  private readonly logger = new Logger(TenantService.name);

  /**
   * One-time backfill on service boot. Any seeded role that's missing
   * a permission row for a module picks one up here with the role's
   * default access (or moduleOverrides if set). Safe to run on every
   * boot — existing rows are left alone. Needed when we add a new
   * module (e.g. MARKUP) after tenants have been onboarded.
   */
  async onModuleInit(): Promise<void> {
    try {
      const roles = await this.roleRepo.find();
      if (roles.length === 0) return;
      const allModules = Object.values(ModuleName);
      let inserted = 0;
      for (const role of roles) {
        const seed = SEED_ROLES.find((s) => s.name === role.name);
        for (const mod of allModules) {
          const exists = await this.rolePermRepo.findOne({
            where: { role: { id: role.id }, module: mod, tenant_id: role.tenant_id },
          });
          if (exists) continue;
          // Impersonate ships OFF for everyone except the Admin role (see
          // onboard() for rationale) — never inherited from defaultAccess.
          const access =
            mod === ModuleName.IMPERSONATE
              ? seed?.name === 'Admin'
                ? AccessLevel.VIEW
                : AccessLevel.NONE
              : MASTER_MODULES_VIEW_DEFAULT.has(mod)
                ? seed?.name === 'Admin'
                  ? seed.defaultAccess
                  : AccessLevel.VIEW
                : seed
                  ? (seed.moduleOverrides?.[mod] ?? seed.defaultAccess)
                  : AccessLevel.NONE; // custom roles default to NONE for newly-added modules
          await this.rolePermRepo.save({
            role,
            module: mod,
            access_level: access,
            tenant_id: role.tenant_id,
            created_by: null,
            updated_by: null,
          });
          inserted++;
        }
      }
      if (inserted > 0) {
        this.logger.log(`Permission backfill: inserted ${inserted} missing rows`);
        // Flush the Redis permission cache so the new rows show up
        // immediately instead of waiting up to CACHE_TTL (5 min). Without
        // this a new module's nav entry stays hidden for up to 5 min
        // even after a re-login, because PermissionGuard /
        // PermissionCheckService still serve the pre-backfill set from
        // cache. Best-effort — a Redis hiccup mustn't crash boot.
        try {
          await this.redis.invalidateByPattern('perm:role:*');
          this.logger.log('Permission cache flushed (perm:role:*)');
        } catch (cacheErr) {
          this.logger.warn(`Permission cache flush skipped: ${(cacheErr as Error).message}`);
        }
      }
    } catch (err) {
      this.logger.warn(`Permission backfill skipped: ${(err as Error).message}`);
    }
  }

  constructor(
    @InjectRepository(TenantEntity)
    private readonly tenantRepo: Repository<TenantEntity>,
    @InjectRepository(UserEntity)
    private readonly userRepo: Repository<UserEntity>,
    @InjectRepository(RoleEntity)
    private readonly roleRepo: Repository<RoleEntity>,
    @InjectRepository(RolePermissionEntity)
    private readonly rolePermRepo: Repository<RolePermissionEntity>,
    private readonly redis: RedisService,
  ) {}

  /**
   * Onboard a new tenant:
   * 1. Create tenant record
   * 2. Seed predefined roles with default permissions
   * 3. Create Admin user assigned to Admin role
   *
   * This is a transactional operation.
   */
  async onboard(dto: OnboardTenantDto) {
    // Reserved: first segment of the trashed-media storage tree.
    if (dto.subdomain.trim().toLowerCase() === DELETED_MEDIA_ROOT) {
      throw new BadRequestException('Subdomain is reserved');
    }

    // Check subdomain uniqueness
    const existingTenant = await this.tenantRepo.findOne({
      where: { subdomain: dto.subdomain },
    });
    if (existingTenant) {
      throw new ConflictException('Subdomain already taken');
    }

    // Check admin email uniqueness globally
    const existingUser = await this.userRepo.findOne({
      where: { email: dto.admin_email },
    });
    if (existingUser) {
      throw new ConflictException('Admin email already registered');
    }

    // Run in transaction
    return this.tenantRepo.manager.transaction(async (manager) => {
      // 1. Create tenant
      const tenant = manager.create(TenantEntity, {
        name: dto.tenant_name,
        subdomain: dto.subdomain,
        is_active: true,
      });
      const savedTenant = await manager.save(tenant);
      const tenantId = savedTenant.id;

      this.logger.log(`Tenant created: ${savedTenant.name} (${tenantId})`);

      // 2. Seed roles + permissions
      let adminRoleId: string | null = null;
      const roleIds: string[] = [];
      const allModules = Object.values(ModuleName);

      for (const seedRole of SEED_ROLES) {
        const role = manager.create(RoleEntity, {
          name: seedRole.name,
          description: seedRole.description,
          is_system: true,
          tenant_id: tenantId,
          // Single-control visibility: Admin + Accountant are the only
          // roles that see all data (the booking:delete shortcut was
          // removed 2026-10-10, so the flag must be set explicitly).
          view_all_bookings_queries:
            seedRole.name === 'Admin' || seedRole.name === 'Accountant',
          created_by: null,
          updated_by: null,
        });
        const savedRole = await manager.save(role);

        if (seedRole.name === 'Admin') {
          adminRoleId = savedRole.id;
        }
        roleIds.push(savedRole.id);

        // Create permissions for every module — respecting any
        // per-module overrides on the seed role (e.g. MARKUP = NONE
        // for Sales / Trip Coordinator).
        const permissions = allModules.map((mod) => {
          // Impersonate ("View as user") ships OFF for everyone; only the
          // Admin role gets it (VIEW) so it's usable out of the box, and
          // ops can grant it to other roles from Settings → Roles when
          // needed. Never inherit it from defaultAccess (which would hand
          // it to every VIEW+ role).
          const accessLevel =
            mod === ModuleName.IMPERSONATE
              ? seedRole.name === 'Admin'
                ? AccessLevel.VIEW
                : AccessLevel.NONE
              : MASTER_MODULES_VIEW_DEFAULT.has(mod) && seedRole.name !== 'Admin'
                ? AccessLevel.VIEW
                : (seedRole.moduleOverrides?.[mod] ?? seedRole.defaultAccess);
          return manager.create(RolePermissionEntity, {
            role: savedRole,
            module: mod,
            access_level: accessLevel,
            tenant_id: tenantId,
            created_by: null,
            updated_by: null,
          });
        });

        await manager.save(permissions);

        this.logger.log(
          `Role seeded: ${seedRole.name} with ${seedRole.defaultAccess} access (${savedRole.id})`,
        );
      }

      await this.seedRoleHierarchy(manager, tenantId, roleIds);

      // 3. Create Admin user
      const passwordHash = await bcrypt.hash(dto.admin_password, 10);

      const adminUser = manager.create(UserEntity, {
        name: dto.admin_name,
        email: dto.admin_email,
        password_hash: passwordHash,
        is_active: true,
        tenant_id: tenantId,
        role_id: adminRoleId!,
        created_by: null,
        updated_by: null,
      });
      const savedAdmin = await manager.save(adminUser);

      this.logger.log(`Admin user created: ${savedAdmin.email} (${savedAdmin.id})`);

      // 4. Seed default currencies (before settings — settings references USD)
      let usdCurrencyId: string | null = null;
      for (const curr of SEED_CURRENCIES) {
        const currency = manager.create(CurrencyEntity, {
          ...curr,
          is_active: true,
          is_system: true,
          tenant_id: tenantId,
          created_by: null,
          updated_by: null,
        });
        const saved = await manager.save(currency);
        if (curr.code === 'USD') usdCurrencyId = saved.id;
      }

      this.logger.log(`Default currencies seeded for tenant ${tenantId}`);

      // 5. Seed default vehicle types
      for (const vt of SEED_VEHICLE_TYPES) {
        const vehicleType = manager.create(VehicleTypeEntity, {
          ...vt,
          is_active: true,
          is_system: true,
          tenant_id: tenantId,
          created_by: null,
          updated_by: null,
        });
        await manager.save(vehicleType);
      }

      this.logger.log(`Default vehicle types seeded for tenant ${tenantId}`);

      // 6. Seed default tenant settings (with USD as default currency)
      const settings = manager.create(TenantSettingsEntity, {
        tenant_id: tenantId,
        company_name: dto.tenant_name,
        default_currency_id: usdCurrencyId,
        primary_color: '#10b981',
        secondary_color: '#1e293b',
        distribution_mode: 'round_robin',
        created_by: null,
        updated_by: null,
      });
      await manager.save(settings);

      this.logger.log(`Default settings created for tenant ${tenantId}`);

      // 6. Seed default service types
      for (const st of SEED_SERVICE_TYPES) {
        const serviceType = manager.create(ServiceTypeEntity, {
          ...st,
          is_active: true,
          is_system: true,
          tenant_id: tenantId,
          created_by: null,
          updated_by: null,
        });
        await manager.save(serviceType);
      }

      this.logger.log(`Default service types seeded for tenant ${tenantId}`);

      // 7. Seed default tax types
      for (const tt of SEED_TAX_TYPES) {
        const taxType = manager.create(TaxTypeEntity, {
          ...tt,
          is_active: true,
          is_system: true,
          tenant_id: tenantId,
          created_by: null,
          updated_by: null,
        });
        await manager.save(taxType);
      }

      this.logger.log(`Default tax types seeded for tenant ${tenantId}`);

      return {
        tenant: {
          id: savedTenant.id,
          name: savedTenant.name,
          subdomain: savedTenant.subdomain,
        },
        admin: {
          id: savedAdmin.id,
          name: savedAdmin.name,
          email: savedAdmin.email,
        },
        roles_seeded: SEED_ROLES.map((r) => r.name),
        message: 'Tenant onboarded successfully',
      };
    });
  }

  /**
   * One hierarchy row per seeded role, with no parent. Reporting lines
   * are set later from Settings → Role Hierarchy.
   */
  private async seedRoleHierarchy(
    manager: EntityManager,
    tenantId: string,
    roleIds: string[],
  ): Promise<void> {
    for (const roleId of roleIds) {
      await manager.save(
        manager.create(RoleHierarchyEntity, {
          role_id: roleId,
          parent_role_id: null,
          tenant_id: tenantId,
          created_by: null,
          updated_by: null,
        }),
      );
    }
  }

  /**
   * List all tenants (for internal/admin use only).
   */
  async findAll() {
    return this.tenantRepo.find({
      where: { is_active: true },
      order: { created_at: 'DESC' },
    });
  }
}
