import typer
from google.cloud import bigquery
from google.oauth2 import service_account
import os
import json

from cruxctl.command_groups.bigsnow.orgs.base_orgs import BaseOrgs
from typing import Optional


class BigqueryOrgs(BaseOrgs):
    @staticmethod
    def get_bigquery_client():
        """Get BigQuery client with proper authentication"""
        try:
            print("DEBUG: Attempting to create BigQuery client...")

            # Try to get credentials from environment variable
            creds_json = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS_JSON")
            if creds_json:
                print("DEBUG: Using GOOGLE_APPLICATION_CREDENTIALS_JSON")
                try:
                    credentials_info = json.loads(creds_json)
                    credentials = service_account.Credentials.from_service_account_info(
                        credentials_info
                    )
                    print("DEBUG: Service account credentials loaded successfully")
                except json.JSONDecodeError as e:
                    print(f"DEBUG: Failed to parse credentials JSON: {e}")
                    raise
            else:
                print("DEBUG: Using default application credentials")
                credentials = None

            # Test if we can create a client
            client = bigquery.Client(credentials=credentials)
            print("DEBUG: BigQuery client created successfully")

            # Test a simple query to verify it works
            test_query = "SELECT 1 as test"
            test_job = client.query(test_query)
            test_job.result()  # Wait for completion
            print("DEBUG: Test query executed successfully")

            return client
        except Exception as e:
            print(f"DEBUG: Error in get_bigquery_client: {e}")
            print(
                f"DEBUG: GOOGLE_APPLICATION_CREDENTIALS:"
                f" {os.environ.get('GOOGLE_APPLICATION_CREDENTIALS')}"
            )
            print(
                f"DEBUG: GOOGLE_APPLICATION_CREDENTIALS_JSON set:"
                f" {'GOOGLE_APPLICATION_CREDENTIALS_JSON' in os.environ}"
            )
            raise typer.Exit(1)

    @staticmethod
    def print_table(headers, rows):
        """Print data in simple tabular format without external dependencies"""
        # Calculate column widths
        col_widths = [len(header) for header in headers]
        for row in rows:
            for i, cell in enumerate(row):
                col_widths[i] = max(col_widths[i], len(str(cell)))

        # Create format string
        format_str = " | ".join([f"{{:<{w}}}" for w in col_widths])

        # Print headers
        print(format_str.format(*headers))
        print("-" * (sum(col_widths) + 3 * (len(headers) - 1)))

        # Print rows
        for row in rows:
            print(format_str.format(*[str(cell) for cell in row]))

    def list_all(self):
        """List all organizations from BigQuery in tabular format"""
        try:
            client = self.get_bigquery_client()

            query = """
            SELECT DISTINCT external_id AS org_id, org_code, org_type, company_name
            FROM `crux-data-lake.crux_platform.client_org_projects`
            ORDER BY org_id
            """

            query_job = client.query(query)
            results = query_job.result()

            if results.total_rows == 0:
                print("No organizations found")
                return

            # Prepare data for tabular display
            headers = ["Org ID", "Org Code", "Type", "Company Name"]
            rows = []

            for row in results:
                rows.append([row.org_id, row.org_code, row.org_type, row.company_name])

            self.print_table(headers, rows)

        except Exception as e:
            print(f"Error listing organizations: {e}")
            raise typer.Exit(1)

    def list(self, org_id=None, org_name=None):
        """Get details for organizations from BigQuery in tabular format

        Args:
            org_id: Specific organization ID to search for
            org_name: Organization name to search for (case-insensitive partial match)
        """
        try:
            # Validate that at least one parameter is provided
            if not org_id and not org_name:
                print("Error: Either --org_id or --org_name must be provided")
                raise typer.Exit(1)

            # Validate that only one parameter is provided
            if org_id and org_name:
                print("Error: Please provide either --org_id or --org_name, not both")
                raise typer.Exit(1)

            client = self.get_bigquery_client()

            # Build query based on which parameter was provided
            if org_id:
                # Validate org_id format
                if not org_id.startswith("CXO"):
                    print("Error: Organization ID must start with 'CXO'")
                    raise typer.Exit(1)

                if not org_id[3:].isdigit() or len(org_id) != 8:
                    print(
                        "Error: Organization ID must be in format CXO followed by 5 digits"
                        " (e.g., CXO12345)"
                    )
                    raise typer.Exit(1)

                query = f"""
                SELECT DISTINCT external_id AS org_id, org_code, org_type, company_name
                FROM `crux-data-lake.crux_platform.client_org_projects`
                WHERE external_id = '{org_id}'
                ORDER BY org_id
                """
            else:  # org_name provided
                # Use UPPER() for case-insensitive search with LIKE for partial matching
                query = f"""
                SELECT DISTINCT external_id AS org_id, org_code, org_type, company_name
                FROM `crux-data-lake.crux_platform.client_org_projects`
                WHERE UPPER(company_name) LIKE UPPER('%{org_name}%')
                ORDER BY company_name, org_id
                """

            query_job = client.query(query)
            results = query_job.result()

            if results.total_rows == 0:
                if org_id:
                    print(f"Organization {org_id} not found")
                else:
                    print(f"No organizations found with name containing '{org_name}'")
                return

            # Prepare data for tabular display
            headers = ["Org ID", "Org Code", "Type", "Company Name"]
            rows = []

            for row in results:
                rows.append([row.org_id, row.org_code, row.org_type, row.company_name])

            # Add a summary line if searching by name and multiple results found
            if org_name and results.total_rows > 1:
                print(
                    f"Found {results.total_rows} organizations matching '{org_name}':\n"
                )

            self.print_table(headers, rows)

        except Exception as e:
            print(f"Error getting organization: {e}")
            raise typer.Exit(1)

    # Other methods remain as placeholders for now
    def add(self, org_id):
        """Add a new organization to BigQuery"""
        try:
            # Validate org_id format
            if not org_id.startswith("CXO"):
                print("Error: Organization ID must start with 'CXO'")
                raise typer.Exit(1)

            if not org_id[3:].isdigit() or len(org_id) != 8:
                print(
                    "Error: Organization ID must be in format CXO followed by 5 digits"
                    " (e.g., CXO12345)"
                )
                raise typer.Exit(1)

            # Check if organization already exists in BigQuery
            client = self.get_bigquery_client()
            check_query = f"""
            SELECT external_id
            FROM `crux-data-lake.crux_platform.client_org_projects`
            WHERE external_id = '{org_id}'
            LIMIT 1
            """

            check_job = client.query(check_query)
            check_results = list(check_job.result())

            if check_results:
                print(f"Organization {org_id} already exists in BigQuery")
                raise typer.Exit(1)

            # Prompt for organization details
            print(f"Adding new organization {org_id}")
            print("Please provide the following details:")

            org_code = typer.prompt("Org Code", default=org_id)

            # Validate and prompt for type - must be 'Dedicated', 'General', or 'Shared'
            while True:
                org_type = typer.prompt("Type (Dedicated/General/Shared)")
                if org_type in ["Dedicated", "General", "Shared"]:
                    break
                print("Error: Type must be either 'Dedicated', 'General', or 'Shared'")

            company_name = typer.prompt("Company Name")

            # Prompt for region/source_project
            while True:
                source_project = typer.prompt(
                    "Source Project (crux-data-lake/crux-data-lake-eu)",
                    default="crux-data-lake",
                )
                if source_project in ["crux-data-lake", "crux-data-lake-eu"]:
                    break
                print(
                    "Error: Source Project must be either 'crux-data-lake' or 'crux-data-lake-eu'"
                )

            # Auto-generate project and dataset based on org_id and region
            org_id_lower = org_id.lower()

            # Determine region suffix
            region_suffix = "" if source_project == "crux-data-lake" else "_eu"

            # Generate project name - always follows the pattern crux-client-{org_id}
            project = f"crux-client-{org_id_lower}"

            # Generate dataset name with region suffix
            dataset = f"client_{org_id_lower}{region_suffix}"

            # Show summary and confirm
            print("\nOrganization details to be added:")
            headers = ["Field", "Value"]
            rows = [
                ["Org ID", org_id],
                ["Org Code", org_code],
                ["Type", org_type],
                ["Company Name", company_name],
                ["Source Project", source_project],
                ["Project", project],
                ["Dataset", dataset],
            ]
            self.print_table(headers, rows)

            confirmation = typer.prompt(
                "Type 'ADD' to confirm adding this organization"
            )
            if confirmation != "ADD":
                print("Add operation cancelled")
                raise typer.Exit(1)

            # Insert into BOTH underlying tables
            insert_queries = [
                # Insert into pa_identity table
                f"""
                INSERT INTO `crux-data-lake.crux_federated.pa_identity`
                (external_id, org_code, org_type, company_name)
                VALUES
                ('{org_id}', '{org_code}', '{org_type}', '{company_name}')
                """,
                # Insert into org_projects table
                f"""
                INSERT INTO `crux-data-lake.crux_platform.org_projects`
                (external_id, project, dataset, type, source_project)
                VALUES
                ('{org_id}', '{project}', '{dataset}', '{org_type}', '{source_project}')
                """,
            ]

            try:
                # Execute both insert queries
                for i, insert_query in enumerate(insert_queries):
                    table_name = "pa_identity" if i == 0 else "org_projects"
                    print(f"Inserting into {table_name}...")

                    insert_job = client.query(insert_query)
                    insert_job.result()  # Wait for completion

                print(f"Successfully added organization {org_id} to BigQuery")
                print("✓ Inserted into crux_federated.pa_identity")
                print("✓ Inserted into crux_platform.org_projects")

            except Exception as insert_err:
                print(f"Error inserting into BigQuery: {insert_err}")
                raise typer.Exit(1)

        except Exception as e:
            print(f"Error adding organization: {e}")
            raise typer.Exit(1)

    def delete(self, org_id):
        """Delete an organization from BigQuery (with strict confirmation)"""
        try:
            client = self.get_bigquery_client()

            # First, let's check if the organization exists in either underlying table
            check_underlying_query = f"""
            SELECT
                'org_projects' as table_name,
                external_id
            FROM `crux-data-lake.crux_platform.org_projects`
            WHERE external_id = '{org_id}'

            UNION ALL

            SELECT
                'pa_identity' as table_name,
                external_id
            FROM `crux-data-lake.crux_federated.pa_identity`
            WHERE external_id = '{org_id}'
            """

            check_job = client.query(check_underlying_query)
            check_results = list(check_job.result())

            if not check_results:
                print(f"Organization {org_id} not found in any underlying table")
                raise typer.Exit(1)

            # Show which tables contain this organization
            print(f"Organization {org_id} found in the following tables:")
            for row in check_results:
                print(f"  - {row.table_name}")

            # Now check the view to see if it appears there
            check_view_query = f"""
            SELECT external_id, org_code, org_type, company_name
            FROM `crux-data-lake.crux_platform.client_org_projects`
            WHERE external_id = '{org_id}'
            LIMIT 1
            """

            view_job = client.query(check_view_query)
            view_results = list(view_job.result())

            if view_results:
                row = view_results[0]
                # Display organization details from view
                print("\nOrganization found in client_org_projects view:")
                headers = ["Field", "Value"]
                rows = [
                    ["Org ID", row.external_id],
                    ["Org Code", row.org_code],
                    ["Type", row.org_type],
                    ["Company Name", row.company_name],
                ]
                self.print_table(headers, rows)
            else:
                print(
                    f"\n⚠️  Note: Organization {org_id} exists in underlying"
                    f" tables but doesn't appear in the view"
                )
                print("   This might be due to JOIN conditions in the view")

                # Let's get details from the underlying tables
                details_query = f"""
                SELECT
                    'pa_identity' as source,
                    external_id,
                    org_code,
                    org_type,
                    company_name
                FROM `crux-data-lake.crux_federated.pa_identity`
                WHERE external_id = '{org_id}'

                UNION ALL

                SELECT
                    'org_projects' as source,
                    external_id,
                    NULL as org_code,
                    type as org_type,
                    NULL as company_name
                FROM `crux-data-lake.crux_platform.org_projects`
                WHERE external_id = '{org_id}'
                """

                details_job = client.query(details_query)
                details_results = list(details_job.result())

                print("\nDetails from underlying tables:")
                for detail_row in details_results:
                    print(
                        f"  {detail_row.source}: external_id={detail_row.external_id}, "
                        f"org_code={detail_row.org_code}, org_type={detail_row.org_type}, "
                        f"company_name={detail_row.company_name}"
                    )

            # Strict confirmation warnings
            print(f"\n⚠️  WARNING: This will permanently delete organization {org_id}")
            print("    THIS ACTION CANNOT BE UNDONE!")
            print("    All data associated with this organization will be lost.")

            # First confirmation
            confirmation = typer.prompt("\nType exactly 'DELETE' to confirm deletion")
            if confirmation != "DELETE":
                print("Deletion aborted - confirmation text did not match 'DELETE'")
                raise typer.Exit(1)

            # Double confirmation for safety
            second_confirmation = typer.prompt(
                f"Are you absolutely sure you want to delete {org_id}? Type 'CONFIRM' to proceed"
            )
            if second_confirmation != "CONFIRM":
                print("Deletion aborted - second confirmation failed")
                raise typer.Exit(1)

            # Execute the deletion from BOTH underlying tables
            delete_queries = [
                # Delete from org_projects table
                f"""
                DELETE FROM `crux-data-lake.crux_platform.org_projects`
                WHERE external_id = '{org_id}'
                """,
                # Delete from pa_identity table
                f"""
                DELETE FROM `crux-data-lake.crux_federated.pa_identity`
                WHERE external_id = '{org_id}'
                """,
            ]

            try:
                # Execute both delete queries
                for i, delete_query in enumerate(delete_queries):
                    table_name = "org_projects" if i == 0 else "pa_identity"
                    print(f"Deleting from {table_name}...")

                    delete_job = client.query(delete_query)
                    delete_job.result()

                print(f"\n✅ Successfully deleted organization {org_id} from BigQuery")
                print("✓ Deleted from crux_platform.org_projects")
                print("✓ Deleted from crux_federated.pa_identity")

            except Exception as delete_err:
                print(f"Error deleting from BigQuery: {delete_err}")
                raise typer.Exit(1)

        except Exception as e:
            print(f"Error deleting organization: {e}")
            raise typer.Exit(1)

    def accounts_list(self, org_id):
        print(f"Listing accounts for org {org_id} in BigQuery (not implemented)")

    def accounts_add(self, org_id, locator, acct_id, client_schema):
        print(f"Adding account for org {org_id} in BigQuery (not implemented)")

    def accounts_delete(self, org_id, locator, acct_id):
        print(f"Deleting account for org {org_id} in BigQuery (not implemented)")

    def entitlements_list(self, org_id):
        """List all entitlements for a specific organization"""
        try:
            # Validate org_id format
            if not org_id.startswith("CXO"):
                print("Error: Organization ID must start with 'CXO'")
                raise typer.Exit(1)

            if not org_id[3:].isdigit() or len(org_id) != 8:
                print(
                    "Error: Organization ID must be in format CXO followed by 5 digits"
                    " (e.g., CXO12345)"
                )
                raise typer.Exit(1)

            # Convert to lowercase for table name
            org_id_lower = org_id.lower()

            # Construct the table name according to the pattern
            table_name = (
                f"crux-client-{org_id_lower}.client_{org_id_lower}.subscriptions"
            )

            client = self.get_bigquery_client()

            query = f"""
            SELECT external_id, org_id, cdu_id, service_type, service_start_date, service_end_date
            FROM `{table_name}`
            WHERE external_id = '{org_id}'
            ORDER BY cdu_id
            """

            query_job = client.query(query)
            results = query_job.result()

            if results.total_rows == 0:
                print(f"No entitlements found for organization {org_id}")
                print(f"Checked table: {table_name}")
                return

            # Get all column names from the schema
            headers = [field.name for field in results.schema]
            rows = []

            for row in results:
                # Convert the row to a list of values
                row_data = []
                for field in results.schema:
                    row_data.append(getattr(row, field.name))
                rows.append(row_data)

            print(f"Entitlements for organization {org_id}:")
            print(f"Table: {table_name}")
            self.print_table(headers, rows)

        except Exception as e:
            print(f"Error listing entitlements: {e}")
            # Provide helpful error message for common issues
            if "Not found" in str(e) or "does not exist" in str(e):
                org_id_lower = org_id.lower()
                table_name = (
                    f"crux-client-{org_id_lower}.client_{org_id_lower}.subscriptions"
                )
                print(
                    f"Table {table_name} may not exist or organization may not have entitlements"
                )
            raise typer.Exit(1)

    def entitlements_add(self, org_id, cdu_id):
        """Add an entitlement for an organization"""
        try:
            # Step 1: Validate org_id format
            if not org_id.startswith("CXO"):
                print("Error: Organization ID must start with 'CXO'")
                raise typer.Exit(1)

            if not org_id[3:].isdigit() or len(org_id) != 8:
                print(
                    "Error: Organization ID must be in format CXO followed by 5 digits"
                    " (e.g., CXO12345)"
                )
                raise typer.Exit(1)

            client = self.get_bigquery_client()

            # Step 2: Check if organization exists and get internal org_id
            org_query = f"""
            SELECT id as org_id, external_id, company_name
            FROM `crux-data-lake.crux_federated.pa_identity`
            WHERE external_id = '{org_id}'
            LIMIT 1
            """

            org_job = client.query(org_query)
            org_results = list(org_job.result())

            if not org_results:
                print(f"Error: Organization {org_id} not found")
                raise typer.Exit(1)

            org_info = org_results[0]
            internal_org_id = (
                org_info.org_id
            )  # This is the internal ID for pa_cdu_subscription
            print(f"Organization: {org_info.company_name} ({org_id})")

            # Step 3: Validate CDU exists
            cdu_query = f"""
            SELECT code, name
            FROM `crux-data-lake.crux_federated.pa_cdu`
            WHERE UPPER(code) = UPPER('{cdu_id}')
            LIMIT 1
            """

            cdu_job = client.query(cdu_query)
            cdu_results = list(cdu_job.result())

            if not cdu_results:
                print(f"Error: CDU {cdu_id} not found")
                raise typer.Exit(1)

            cdu_info = cdu_results[0]
            actual_cdu_code = cdu_info.code
            print(f"CDU: {cdu_info.name} ({actual_cdu_code})")

            # Step 4: Check for duplicate entitlements
            duplicate_query = f"""
            SELECT cdu_id
            FROM `crux-data-lake.crux_platform.service_restrictions`
            WHERE external_id = '{org_id}'
            AND cdu_id = '{actual_cdu_code}'
            LIMIT 1
            """

            dup_job = client.query(duplicate_query)
            dup_results = list(dup_job.result())

            if dup_results:
                print(
                    f"Error: Organization {org_id} already has entitlement for"
                    f" CDU {actual_cdu_code}"
                )
                raise typer.Exit(1)

            # Step 5: Prompt for entitlement details
            print("\nEntitlement Details:")

            # Service type
            while True:
                service_type = typer.prompt(
                    "Service Type (Full Access/Restricted)", default="Full Access"
                )
                if service_type in ["Full Access", "Restricted"]:
                    break
                print("Error: Service Type must be 'Full Access' or 'Restricted'")

            # Service dates
            from datetime import datetime, timedelta

            # Default start date: today
            default_start = datetime.now().strftime("%Y-%m-%d")
            service_start_date = typer.prompt(
                "Service Start Date (YYYY-MM-DD)", default=default_start
            )

            # Default end date: 1 year from start
            try:
                start_dt = datetime.strptime(service_start_date, "%Y-%m-%d")
                default_end = (start_dt + timedelta(days=365)).strftime("%Y-%m-%d")
            except Exception:
                default_end = (datetime.now() + timedelta(days=365)).strftime(
                    "%Y-%m-%d"
                )

            service_end_date = typer.prompt(
                "Service End Date (YYYY-MM-DD)", default=default_end
            )

            # Step 6: Show summary and confirm
            print("\n" + "=" * 60)
            print("Entitlement to be added:")
            headers = ["Field", "Value"]
            rows = [
                ["Organization", f"{org_info.company_name} ({org_id})"],
                ["CDU", f"{cdu_info.name} ({actual_cdu_code})"],
                ["Service Type", service_type],
                ["Start Date", service_start_date],
                ["End Date", service_end_date],
            ]
            self.print_table(headers, rows)
            print("=" * 60)

            confirmation = typer.prompt(
                "\nType 'ADD' to confirm adding this entitlement"
            )
            if confirmation != "ADD":
                print("Add operation cancelled")
                raise typer.Exit(1)

            # Step 7: Insert into pa_cdu_subscription table
            # (underlying table for service_restrictions view)
            # Generate a unique ID for the subscription
            import uuid

            subscription_id = str(uuid.uuid4())

            insert_query = f"""
            INSERT INTO `crux-data-lake.crux_federated.pa_cdu_subscription`
            (id, org_id, cdu_id, type, service_type, service_start_date, service_end_date, status)
            VALUES
            ('{subscription_id}', '{internal_org_id}', '{actual_cdu_code}',
            'Full', '{service_type}', DATE'{service_start_date}', DATE'{service_end_date}',
             'active')
            """

            try:
                insert_job = client.query(insert_query)
                insert_job.result()  # Wait for completion
                print(
                    f"\n✓ Successfully added entitlement for {org_id} to CDU {actual_cdu_code}"
                )

                # Show next steps
                print("\nNext Steps:")
                print("  1. Provision views:")
                print("     cruxctl bigsnow --instance bigquery orgs provision-add \\")
                print(f"       --org_id {org_id} \\")
                print("       --view_name <VIEW_NAME>")
                print("\n  2. Find available views:")
                print(
                    f"cruxctl bigsnow --instance bigquery orgs provision-missing-views "
                    f"--org_id {org_id}"
                )

            except Exception as insert_err:
                print(f"Error inserting entitlement: {insert_err}")
                raise typer.Exit(1)

        except Exception as e:
            print(f"Error adding entitlement: {e}")
            raise typer.Exit(1)

    def cdu_provision_list(self, cdu_id: str, region: Optional[str] = None):
        """List all organizations provisioned for a specific CDU"""
        try:
            # Validate cdu_id format
            if not cdu_id:
                print("Error: CDU ID is required")
                raise typer.Exit(1)

            client = self.get_bigquery_client()

            # Find the CDU with case-insensitive search
            cdu_query = f"""
            SELECT id, code
            FROM `crux-data-lake.crux_federated.pa_cdu`
            WHERE UPPER(code) = UPPER('{cdu_id}')
            LIMIT 1
            """

            cdu_job = client.query(cdu_query)
            cdu_results = list(cdu_job.result())

            if not cdu_results:
                print(f"Error: CDU {cdu_id} not found")
                raise typer.Exit(1)

            actual_cdu_code = cdu_results[0].code
            print(f"CDU: {actual_cdu_code}")
            print("=" * 60)

            # Get all organizations entitled to this CDU
            # Note: service_restrictions view has limited columns, so we join with pa_identity
            entitlements_query = f"""
            SELECT
                sr.external_id,
                sr.cdu_id,
                sr.type as subscription_type,
                sr.service_type,
                sr.validFrom,
                sr.validTo,
                pi.org_code,
                pi.company_name
            FROM `crux-data-lake.crux_platform.service_restrictions` sr
            LEFT JOIN `crux-data-lake.crux_federated.pa_identity` pi
                ON sr.external_id = pi.external_id
            WHERE sr.cdu_id = '{actual_cdu_code}'
            ORDER BY sr.external_id
            """

            ent_job = client.query(entitlements_query)
            entitlements = list(ent_job.result())

            if not entitlements:
                print(f"No organizations are entitled to CDU {actual_cdu_code}")
                return

            print(
                f"\nFound {len(entitlements)} organization(s) entitled to {actual_cdu_code}\n"
            )

            # For each entitled org, check provisioning status
            provision_data = []

            for ent in entitlements:
                org_id = ent.external_id

                # Get org project info
                org_project_query = f"""
                SELECT project, dataset, source_project
                FROM `crux-data-lake.crux_platform.org_projects`
                WHERE external_id = '{org_id}'
                LIMIT 1
                """

                try:
                    proj_job = client.query(org_project_query)
                    proj_results = list(proj_job.result())

                    if not proj_results:
                        provision_data.append(
                            {
                                "org_id": org_id,
                                "org_code": ent.org_code or "N/A",
                                "org_name": ent.company_name or "N/A",
                                "subscription_type": ent.subscription_type or "N/A",
                                "valid_from": (
                                    ent.validFrom.strftime("%Y-%m-%d")
                                    if ent.validFrom
                                    else "N/A"
                                ),
                                "valid_to": (
                                    ent.validTo.strftime("%Y-%m-%d")
                                    if ent.validTo
                                    else "N/A"
                                ),
                                "project_status": "NOT_REGISTERED",
                                "views_provisioned": 0,
                                "views_expected": 0,
                            }
                        )
                        continue

                    proj_info = proj_results[0]
                    client_project = proj_info.project
                    client_dataset = proj_info.dataset
                    source_project = proj_info.source_project

                    # Check if dataset exists
                    dataset_id = f"{client_project}.{client_dataset}"
                    try:
                        client.get_dataset(dataset_id)
                        dataset_exists = True
                    except Exception:
                        dataset_exists = False

                    if not dataset_exists:
                        provision_data.append(
                            {
                                "org_id": org_id,
                                "org_code": ent.org_code or "N/A",
                                "org_name": ent.company_name or "N/A",
                                "subscription_type": ent.subscription_type or "N/A",
                                "valid_from": (
                                    ent.validFrom.strftime("%Y-%m-%d")
                                    if ent.validFrom
                                    else "N/A"
                                ),
                                "valid_to": (
                                    ent.validTo.strftime("%Y-%m-%d")
                                    if ent.validTo
                                    else "N/A"
                                ),
                                "project_status": "DATASET_NOT_CREATED",
                                "views_provisioned": 0,
                                "views_expected": 0,
                            }
                        )
                        continue

                    # Get expected views from catalog
                    catalog_query = f"""
                    SELECT view_name
                    FROM `{source_project}.crux_platform.catalog`
                    WHERE cdu_id = '{actual_cdu_code}'
                    """

                    cat_job = client.query(catalog_query)
                    expected_views = set([row.view_name for row in cat_job.result()])

                    # Get provisioned views
                    views_query = f"""
                    SELECT table_name
                    FROM `{client_project}.{client_dataset}.INFORMATION_SCHEMA.TABLES`
                    WHERE table_type = 'VIEW'
                    AND table_name LIKE '{actual_cdu_code}__%'
                    """

                    try:
                        views_job = client.query(views_query)
                        provisioned_views = set(
                            [row.table_name for row in views_job.result()]
                        )
                    except Exception:
                        provisioned_views = set()

                    provision_data.append(
                        {
                            "org_id": org_id,
                            "org_code": ent.org_code or "N/A",
                            "org_name": ent.company_name or "N/A",
                            "subscription_type": ent.subscription_type or "N/A",
                            "valid_from": (
                                ent.validFrom.strftime("%Y-%m-%d")
                                if ent.validFrom
                                else "N/A"
                            ),
                            "valid_to": (
                                ent.validTo.strftime("%Y-%m-%d")
                                if ent.validTo
                                else "N/A"
                            ),
                            "project_status": "ACTIVE",
                            "views_provisioned": len(provisioned_views),
                            "views_expected": len(expected_views),
                        }
                    )

                except Exception as e:
                    provision_data.append(
                        {
                            "org_id": org_id,
                            "org_code": ent.org_code or "N/A",
                            "org_name": ent.company_name or "N/A",
                            "subscription_type": ent.subscription_type or "N/A",
                            "valid_from": "N/A",
                            "valid_to": "N/A",
                            "project_status": f"ERROR: {str(e)[:30]}",
                            "views_provisioned": 0,
                            "views_expected": 0,
                        }
                    )

            # Display results
            headers = [
                "Org ID",
                "Org Code",
                "Company Name",
                "Subscription",
                "Valid From",
                "Valid To",
                "Status",
                "Views",
            ]

            rows = []
            for data in provision_data:
                views_status = f"{data['views_provisioned']}/{data['views_expected']}"
                if data["views_provisioned"] < data["views_expected"]:
                    views_status += " ⚠"

                rows.append(
                    [
                        data["org_id"],
                        data["org_code"],
                        data["org_name"][:25],  # Truncate long names
                        data["subscription_type"][:15],
                        data["valid_from"],
                        data["valid_to"],
                        data["project_status"],
                        views_status,
                    ]
                )

            self.print_table(headers, rows)

            # Summary
            total_orgs = len(provision_data)
            active_orgs = len(
                [d for d in provision_data if d["project_status"] == "ACTIVE"]
            )
            fully_provisioned = len(
                [
                    d
                    for d in provision_data
                    if d["project_status"] == "ACTIVE"
                    and d["views_provisioned"] == d["views_expected"]
                    and d["views_expected"] > 0
                ]
            )

            print("\nSummary:")
            print(f"  Total Organizations: {total_orgs}")
            print(f"  Active Projects: {active_orgs}")
            print(f"  Fully Provisioned: {fully_provisioned}")

        except Exception as e:
            print(f"Error listing CDU provisioning: {e}")
            raise typer.Exit(1)

    def provision_list(
        self, org_id: Optional[str] = None, cdu_id: Optional[str] = None
    ):
        """
        List provisioning information for organizations or CDUs

        Args:
            org_id: Organization ID to list provisioning for
            cdu_id: CDU ID to list all orgs provisioned for this CDU
        """
        try:
            # Validate that exactly one parameter is provided
            if not org_id and not cdu_id:
                print("Error: Either --org_id or --cdu_id must be provided")
                raise typer.Exit(1)

            if org_id and cdu_id:
                print("Error: Please provide either --org_id or --cdu_id, not both")
                raise typer.Exit(1)

            # Route to appropriate handler
            if cdu_id:
                self._provision_list_by_cdu(cdu_id)
            else:
                self._provision_list_by_org(org_id)

        except Exception as e:
            print(f"Error listing provisioning: {e}")
            raise typer.Exit(1)

    def _provision_list_by_org(self, org_id: str):
        """List provisioning for a specific organization (original logic)"""
        # Validate org_id format
        if not org_id.startswith("CXO"):
            print("Error: Organization ID must start with 'CXO'")
            raise typer.Exit(1)

        org_id_lower = org_id.lower()
        project_name = f"crux-client-{org_id_lower}"
        dataset_name = f"client_{org_id_lower}"

        client = self.get_bigquery_client()

        # Get subscribed CDUs
        subscriptions_query = f"""
        SELECT cdu_id, service_type, validFrom, validTo
        FROM `{project_name}.{dataset_name}.subscriptions`
        ORDER BY cdu_id
        """

        # Get provisioned views
        views_query = f"""
        SELECT table_name as view_name, creation_time
        FROM `{project_name}.{dataset_name}.INFORMATION_SCHEMA.TABLES`
        WHERE table_type = 'VIEW'
        ORDER BY table_name
        """

        # Execute both queries
        subs_job = client.query(subscriptions_query)
        views_job = client.query(views_query)

        subscriptions = list(subs_job.result())
        views = list(views_job.result())

        print(f"Provisioning Summary for {org_id}:")
        print(f"Subscribed CDUs: {len(subscriptions)}")
        print(f"Provisioned Views: {len(views)}")

        if subscriptions:
            print("\nSubscriptions:")
            sub_headers = ["CDU ID", "Service Type", "Valid From", "Valid To"]
            sub_rows = []
            for sub in subscriptions:
                valid_from = (
                    sub.validFrom.strftime("%Y-%m-%d") if sub.validFrom else "N/A"
                )
                valid_to = sub.validTo.strftime("%Y-%m-%d") if sub.validTo else "N/A"
                sub_rows.append([sub.cdu_id, sub.service_type, valid_from, valid_to])
            self.print_table(sub_headers, sub_rows)

        if views:
            print("\nProvisioned Views:")
            view_headers = ["View Name", "CDU", "Created", "Status"]
            view_rows = []
            for view in views:
                cdu = view.view_name.split("__")[0] if "__" in view.view_name else "N/A"
                created = (
                    view.creation_time.strftime("%Y-%m-%d")
                    if view.creation_time
                    else "N/A"
                )

                # Check if view corresponds to a subscribed CDU
                status = (
                    "ACTIVE"
                    if any(sub.cdu_id == cdu for sub in subscriptions)
                    else "ORPHANED"
                )

                view_rows.append([view.view_name, cdu, created, status])
            self.print_table(view_headers, view_rows)

    def _provision_list_by_cdu(self, cdu_id: str):
        """List all organizations provisioned for a specific CDU"""
        if not cdu_id:
            print("Error: CDU ID is required")
            raise typer.Exit(1)

        client = self.get_bigquery_client()

        # Find the CDU with case-insensitive search
        cdu_query = f"""
        SELECT id, code
        FROM `crux-data-lake.crux_federated.pa_cdu`
        WHERE UPPER(code) = UPPER('{cdu_id}')
        LIMIT 1
        """

        cdu_job = client.query(cdu_query)
        cdu_results = list(cdu_job.result())

        if not cdu_results:
            print(f"Error: CDU {cdu_id} not found")
            raise typer.Exit(1)

        actual_cdu_code = cdu_results[0].code
        print(f"CDU: {actual_cdu_code}")
        print("=" * 60)

        # Get all organizations entitled to this CDU
        entitlements_query = f"""
        SELECT
            sr.external_id,
            sr.cdu_id,
            sr.type as subscription_type,
            sr.service_type,
            sr.validFrom,
            sr.validTo,
            pi.org_code,
            pi.company_name
        FROM `crux-data-lake.crux_platform.service_restrictions` sr
        LEFT JOIN `crux-data-lake.crux_federated.pa_identity` pi
            ON sr.external_id = pi.external_id
        WHERE sr.cdu_id = '{actual_cdu_code}'
        ORDER BY sr.external_id
        """

        ent_job = client.query(entitlements_query)
        entitlements = list(ent_job.result())

        if not entitlements:
            print(f"No organizations are entitled to CDU {actual_cdu_code}")
            return

        print(
            f"\nFound {len(entitlements)} organization(s) entitled to {actual_cdu_code}\n"
        )

        # For each entitled org, check provisioning status
        provision_data = []

        for ent in entitlements:
            org_id = ent.external_id

            # Get org project info
            org_project_query = f"""
            SELECT project, dataset, source_project
            FROM `crux-data-lake.crux_platform.org_projects`
            WHERE external_id = '{org_id}'
            LIMIT 1
            """

            try:
                proj_job = client.query(org_project_query)
                proj_results = list(proj_job.result())

                if not proj_results:
                    provision_data.append(
                        {
                            "org_id": org_id,
                            "org_code": ent.org_code or "N/A",
                            "org_name": ent.company_name or "N/A",
                            "subscription_type": ent.subscription_type or "N/A",
                            "valid_from": (
                                ent.validFrom.strftime("%Y-%m-%d")
                                if ent.validFrom
                                else "N/A"
                            ),
                            "valid_to": (
                                ent.validTo.strftime("%Y-%m-%d")
                                if ent.validTo
                                else "N/A"
                            ),
                            "project_status": "NOT_REGISTERED",
                            "views_provisioned": 0,
                            "views_expected": 0,
                        }
                    )
                    continue

                proj_info = proj_results[0]
                client_project = proj_info.project
                client_dataset = proj_info.dataset
                source_project = proj_info.source_project

                # Check if dataset exists
                dataset_id = f"{client_project}.{client_dataset}"
                try:
                    client.get_dataset(dataset_id)
                    dataset_exists = True
                except Exception:
                    dataset_exists = False

                if not dataset_exists:
                    provision_data.append(
                        {
                            "org_id": org_id,
                            "org_code": ent.org_code or "N/A",
                            "org_name": ent.company_name or "N/A",
                            "subscription_type": ent.subscription_type or "N/A",
                            "valid_from": (
                                ent.validFrom.strftime("%Y-%m-%d")
                                if ent.validFrom
                                else "N/A"
                            ),
                            "valid_to": (
                                ent.validTo.strftime("%Y-%m-%d")
                                if ent.validTo
                                else "N/A"
                            ),
                            "project_status": "DATASET_NOT_CREATED",
                            "views_provisioned": 0,
                            "views_expected": 0,
                        }
                    )
                    continue

                # Get expected views from catalog
                catalog_query = f"""
                SELECT view_name
                FROM `{source_project}.crux_platform.catalog`
                WHERE cdu_id = '{actual_cdu_code}'
                """

                cat_job = client.query(catalog_query)
                expected_views = set([row.view_name for row in cat_job.result()])

                # Get provisioned views
                views_query = f"""
                SELECT table_name
                FROM `{client_project}.{client_dataset}.INFORMATION_SCHEMA.TABLES`
                WHERE table_type = 'VIEW'
                AND table_name LIKE '{actual_cdu_code}__%'
                """

                try:
                    views_job = client.query(views_query)
                    provisioned_views = set(
                        [row.table_name for row in views_job.result()]
                    )
                except Exception:
                    provisioned_views = set()

                provision_data.append(
                    {
                        "org_id": org_id,
                        "org_code": ent.org_code or "N/A",
                        "org_name": ent.company_name or "N/A",
                        "subscription_type": ent.subscription_type or "N/A",
                        "valid_from": (
                            ent.validFrom.strftime("%Y-%m-%d")
                            if ent.validFrom
                            else "N/A"
                        ),
                        "valid_to": (
                            ent.validTo.strftime("%Y-%m-%d") if ent.validTo else "N/A"
                        ),
                        "project_status": "ACTIVE",
                        "views_provisioned": len(provisioned_views),
                        "views_expected": len(expected_views),
                    }
                )

            except Exception as e:
                provision_data.append(
                    {
                        "org_id": org_id,
                        "org_code": ent.org_code or "N/A",
                        "org_name": ent.company_name or "N/A",
                        "subscription_type": ent.subscription_type or "N/A",
                        "valid_from": "N/A",
                        "valid_to": "N/A",
                        "project_status": f"ERROR: {str(e)[:30]}",
                        "views_provisioned": 0,
                        "views_expected": 0,
                    }
                )

        # Display results
        headers = [
            "Org ID",
            "Org Code",
            "Company Name",
            "Subscription",
            "Valid From",
            "Valid To",
            "Status",
            "Views",
        ]

        rows = []
        for data in provision_data:
            views_status = f"{data['views_provisioned']}/{data['views_expected']}"
            if data["views_provisioned"] < data["views_expected"]:
                views_status += " ⚠"

            rows.append(
                [
                    data["org_id"],
                    data["org_code"],
                    data["org_name"][:25],  # Truncate long names
                    data["subscription_type"][:15],
                    data["valid_from"],
                    data["valid_to"],
                    data["project_status"],
                    views_status,
                ]
            )

        self.print_table(headers, rows)

        # Summary
        total_orgs = len(provision_data)
        active_orgs = len(
            [d for d in provision_data if d["project_status"] == "ACTIVE"]
        )
        fully_provisioned = len(
            [
                d
                for d in provision_data
                if d["project_status"] == "ACTIVE"
                and d["views_provisioned"] == d["views_expected"]
                and d["views_expected"] > 0
            ]
        )

        print("\nSummary:")
        print(f"  Total Organizations: {total_orgs}")
        print(f"  Active Projects: {active_orgs}")
        print(f"  Fully Provisioned: {fully_provisioned}")

    def provision_add(
        self,
        org_id: Optional[str] = None,
        view_name: Optional[str] = None,
        region: Optional[str] = None,
        cdu_id: Optional[str] = None,
        dry_run: bool = False,
    ):
        """
        Add provisioning for views in BigQuery

        Args:
            org_id: Organization ID to provision for
            view_name: Specific view name to provision (used with org_id)
            region: Region (not used in BigQuery, kept for interface compatibility)
            cdu_id: CDU ID to provision (provisions for all entitled orgs)
            dry_run: If True, only simulate the provisioning
        """
        try:
            # Validate parameters - need either org_id or cdu_id
            if not org_id and not cdu_id:
                print("Error: Either --org_id or --cdu_id must be provided")
                raise typer.Exit(1)

            if org_id and cdu_id:
                print("Error: Please provide either --org_id or --cdu_id, not both")
                raise typer.Exit(1)

            # Route based on parameters
            if cdu_id:
                # Provision CDU for all entitled orgs
                self._provision_add_by_cdu(cdu_id, dry_run)
            elif org_id and view_name:
                # Provision specific view for org
                self._provision_add_by_org(org_id, view_name, dry_run)
            elif org_id:
                # Provision all views for org (view_name is None)
                self._provision_add_by_org(org_id, None, dry_run)

        except Exception as e:
            print(f"Error in provision_add: {e}")
            raise typer.Exit(1)

    def _provision_add_by_org(
        self, org_id: str, specific_cdu_id: Optional[str] = None, dry_run: bool = False
    ):
        """Add provisioning for a specific organization"""

        # Validate org_id format
        if not org_id.startswith("CXO"):
            print("Error: Organization ID must start with 'CXO'")
            raise typer.Exit(1)

        if not org_id[3:].isdigit() or len(org_id) != 8:
            print(
                "Error: Organization ID must be in format CXO followed by 5 digits (e.g., CXO12345)"
            )
            raise typer.Exit(1)

        client = self.get_bigquery_client()
        org_id_lower = org_id.lower()

        # If specific_cdu_id looks like a view_name (contains __), extract the CDU
        view_name_to_provision = None
        if specific_cdu_id and "__" in specific_cdu_id:
            view_name_to_provision = specific_cdu_id
            # Extract CDU from view name (e.g., "cx42749__TABLE__ID" -> "cx42749")
            specific_cdu_id = specific_cdu_id.split("__")[0]
            print(
                f"Extracted CDU ID: {specific_cdu_id} from view name: {view_name_to_provision}"
            )

        print(
            f"{'[DRY RUN] ' if dry_run else ''}Provisioning views for organization {org_id}"
        )

        # Step 1: Get organization information
        org_query = f"""
        SELECT id, external_id, org_code, org_type, company_name
        FROM `crux-data-lake.crux_federated.pa_identity`
        WHERE external_id = '{org_id}'
        LIMIT 1
        """

        org_job = client.query(org_query)
        org_results = list(org_job.result())

        if not org_results:
            print(f"Error: Organization {org_id} not found")
            raise typer.Exit(1)

        org_info = org_results[0]
        print(f"Organization: {org_info.company_name} ({org_info.org_code})")

        # Step 2: Determine source project and region
        source_project_query = f"""
        SELECT source_project, project, dataset, type
        FROM `crux-data-lake.crux_platform.org_projects`
        WHERE external_id = '{org_id}'
        LIMIT 1
        """

        source_project_job = client.query(source_project_query)
        source_project_results = list(source_project_job.result())

        if not source_project_results:
            print(f"Error: Organization {org_id} not found in org_projects")
            print("Please run 'orgs add' first to register this organization")
            raise typer.Exit(1)

        project_info = source_project_results[0]
        source_project = project_info.source_project
        client_project = project_info.project
        client_dataset = project_info.dataset

        # Determine location based on source project
        location = "US" if source_project == "crux-data-lake" else "EU"

        print(f"Source: {source_project} (Region: {location})")
        print(f"Target: {client_project}.{client_dataset}")

        # Step 3: Ensure client dataset exists
        dataset_id = f"{client_project}.{client_dataset}"
        try:
            client.get_dataset(dataset_id)
            print(f"✓ Dataset exists: {dataset_id}")
        except Exception:
            if dry_run:
                print(f"[DRY RUN] Would create dataset: {dataset_id}")
            else:
                print(f"Creating dataset: {dataset_id}")
                dataset = bigquery.Dataset(dataset_id)
                dataset.location = location
                dataset.description = f"{org_info.org_code} - {org_info.company_name}"
                dataset.labels = {
                    "kind": "client_views",
                    "org_id": org_id_lower,
                    "source_project": source_project,
                    "region": location.lower(),
                }
                try:
                    dataset = client.create_dataset(dataset, timeout=30)
                    print(f"✓ Created dataset: {dataset_id}")
                except Exception as create_err:
                    print(f"Error creating dataset: {create_err}")
                    raise typer.Exit(1)

        # Step 4: Get entitled CDUs for this organization
        if specific_cdu_id:
            entitled_cdus_query = f"""
            SELECT cdu_id, service_type, validFrom, validTo
            FROM `crux-data-lake.crux_platform.service_restrictions`
            WHERE external_id = '{org_id}' AND cdu_id = '{specific_cdu_id}'
            """
        else:
            entitled_cdus_query = f"""
            SELECT cdu_id, service_type, validFrom, validTo
            FROM `crux-data-lake.crux_platform.service_restrictions`
            WHERE external_id = '{org_id}'
            ORDER BY cdu_id
            """

        entitled_job = client.query(entitled_cdus_query)
        entitled_cdus = list(entitled_job.result())

        if not entitled_cdus:
            if specific_cdu_id:
                print(
                    f"Error: Organization {org_id} is not entitled to CDU {specific_cdu_id}"
                )
            else:
                print(f"Warning: Organization {org_id} has no entitlements")
            raise typer.Exit(1)

        print(f"\nFound {len(entitled_cdus)} entitled CDU(s)")

        # Step 5: Create subscriptions view (required for entitlement filtering)
        subscriptions_view_id = f"{dataset_id}.subscriptions"
        try:
            client.get_table(subscriptions_view_id)
            print(f"✓ Subscriptions view exists: {subscriptions_view_id}")
        except Exception:
            if dry_run:
                print(
                    f"[DRY RUN] Would create subscriptions view: {subscriptions_view_id}"
                )
            else:
                print(f"Creating subscriptions view: {subscriptions_view_id}")
                create_subscriptions_ddl = f"""
                CREATE OR REPLACE VIEW `{subscriptions_view_id}` AS
                SELECT *
                FROM `{source_project}.crux_platform.service_restrictions`
                WHERE external_id = '{org_id}'
                ORDER BY cdu_id
                """
                try:
                    create_job = client.query(create_subscriptions_ddl)
                    create_job.result()
                    print("✓ Created subscriptions view")
                except Exception as create_err:
                    print(f"Error creating subscriptions view: {create_err}")
                    raise typer.Exit(1)

        # Step 6: Get catalog of available tables for entitled CDUs
        cdu_codes = [cdu.cdu_id for cdu in entitled_cdus]
        cdu_in_list = "'" + "','".join(cdu_codes) + "'"

        catalog_query = f"""
        SELECT
            cdu_id,
            table_name,
            view_name,
            dataset_legacy_id,
            table_dataset,
            product_family,
            CASE
                WHEN product_family = 'Custom Wrangle' THEN
                    CONCAT('crux-data-prd.wrangle_', cdu_id, '.', table_name)
                ELSE
                    CONCAT('{source_project}', '.', table_dataset, '.', table_name)
            END as full_table_id
        FROM `{source_project}.crux_platform.catalog`
        WHERE cdu_id IN ({cdu_in_list})
        AND table_dataset IS NOT NULL
        ORDER BY cdu_id, view_name
        """

        catalog_job = client.query(catalog_query)
        catalog_entries = list(catalog_job.result())

        if not catalog_entries:
            print("Warning: No tables found in catalog for entitled CDUs")
            print("Entitled CDUs may not have any published data yet")
            return

        print(f"Found {len(catalog_entries)} table(s) in catalog")

        # Step 7: Get Crux curated schemas for field descriptions
        crux_schema_query = f"""
        SELECT
            cdu_id,
            table_name,
            field_name,
            data_type,
            ord,
            description,
            cxref,
            pkey
        FROM `{source_project}.crux_platform.crux_schema`
        WHERE cdu_id IN ({cdu_in_list})
        ORDER BY cdu_id, table_name, ord
        """

        schema_job = client.query(crux_schema_query)
        crux_schemas = {}

        for row in schema_job.result():
            key = f"{row.cdu_id}__{row.table_name}"
            if key not in crux_schemas:
                crux_schemas[key] = {}
            crux_schemas[key][row.ord] = {
                "field_name": row.field_name,
                "data_type": row.data_type,
                "description": row.description,
                "cxref": row.cxref,
                "pkey": row.pkey,
            }

        # Step 8: Get current table schemas from INFORMATION_SCHEMA
        # Get unique (full_table_id, table_name) combinations from catalog
        table_info = list(
            set([(entry.full_table_id, entry.table_name) for entry in catalog_entries])
        )

        region_location = f"region-{location.lower()}"

        print(f"DEBUG: Querying schemas for {len(table_info)} unique table(s)")

        # Query INFORMATION_SCHEMA for each table from the correct project
        table_schemas = {}
        for full_table_id, table_name in table_info:
            # Extract project and dataset from full_table_id
            # Format: project.dataset.table
            parts = full_table_id.split(".")
            if len(parts) != 3:
                print(f"Warning: Invalid full_table_id format: {full_table_id}")
                continue

            schema_project = parts[0]
            dataset = parts[1]

            print(f"DEBUG: Querying {schema_project}.{dataset}.{table_name}")

            # For crux-data-prd, use the dataset name directly
            # without region prefix in INFORMATION_SCHEMA
            # For crux-data-lake/crux-data-lake-eu, use region-us or region-eu prefix
            if schema_project == "crux-data-prd":
                info_schema_location = dataset
            else:
                info_schema_location = region_location

            table_schema_query = f"""
            SELECT
                table_name,
                ordinal_position,
                column_name,
                REPLACE(REPLACE(REPLACE(data_type, 'FLOAT64', 'FLOAT'), 'INT64', 'INTEGER'),
                 'BOOL', 'BOOLEAN') as data_type
            FROM `{schema_project}.{info_schema_location}.INFORMATION_SCHEMA.COLUMNS`
            WHERE table_schema = '{dataset}'
            AND table_name = '{table_name}'
            ORDER BY ordinal_position
            """

            try:
                schema_query_job = client.query(table_schema_query)
                row_count = 0
                for row in schema_query_job.result():
                    row_count += 1
                    if row.table_name not in table_schemas:
                        table_schemas[row.table_name] = {}
                    table_schemas[row.table_name][row.ordinal_position] = {
                        "field_name": row.column_name,
                        "data_type": row.data_type,
                    }
                print(f"DEBUG: Found {row_count} columns for {table_name}")
            except Exception as e:
                print(
                    f"Warning: Could not get schema for "
                    f"{schema_project}.{dataset}.{table_name}: {e}"
                )
                continue

        # Step 9: Create views for each catalog entry
        created_views = []
        skipped_views = []
        errors = []

        for entry in catalog_entries:
            view_name = entry.view_name

            # If we're provisioning a specific view, skip all others
            if view_name_to_provision and view_name != view_name_to_provision:
                continue

            view_id = f"{dataset_id}.{view_name}"
            cdu_code = entry.cdu_id
            table_name = entry.table_name
            full_table_id = entry.full_table_id
            dataset_legacy_id = entry.dataset_legacy_id
            product_family = entry.product_family

            # Get schema information
            schema_key = f"{cdu_code}__{table_name}"
            crux_schema = crux_schemas.get(schema_key, {})
            table_schema = table_schemas.get(table_name, {})

            if not table_schema:
                errors.append(f"No schema found for table {table_name}")
                continue

            # Build field list with type casting and aliasing
            fields = []
            field_descriptions = {}
            has_crux_supplied_dt = False

            for pos in sorted(table_schema.keys()):
                table_field = table_schema[pos]
                crux_field = crux_schema.get(pos, table_field)

                table_name_field = table_field["field_name"]
                crux_name = crux_field.get("field_name", table_name_field)
                table_type = table_field["data_type"]
                crux_type = crux_field.get("data_type", table_type)

                # Check for crux_supplied_dt
                if crux_name == "crux_supplied_dt":
                    has_crux_supplied_dt = True

                # Build field expression with casting/aliasing if needed
                if table_type != crux_type:
                    type_map = {
                        "FLOAT": "FLOAT64",
                        "INTEGER": "INT64",
                        "BOOLEAN": "BOOL",
                    }
                    bq_type = type_map.get(crux_type, crux_type)
                    field_expr = (
                        f"CAST(`{table_name_field}` AS {bq_type}) AS {crux_name}"
                    )
                elif table_name_field != crux_name:
                    field_expr = f"`{table_name_field}` AS {crux_name}"
                else:
                    field_expr = f"`{table_name_field}`"

                fields.append(field_expr)

                # Store field description if available
                if crux_field.get("description"):
                    field_descriptions[crux_name] = crux_field["description"]

            # Build entitlement clause
            entitlement_date = (
                "crux_supplied_dt" if has_crux_supplied_dt else "CURRENT_DATETIME()"
            )
            entitlement_clause = f"""{entitlement_date} >= (
                SELECT CASE
                    WHEN validFrom IS NULL THEN DATETIME '1970-12-31'
                    ELSE PARSE_DATETIME('%Y-%m-%d', validFrom)
                END
                FROM `{dataset_id}.subscriptions`
                WHERE cdu_id = '{cdu_code}'
            ) AND {entitlement_date} <= (
                SELECT CASE
                    WHEN validTo IS NULL THEN DATETIME '2050-12-31'
                    ELSE PARSE_DATETIME('%Y-%m-%d', validTo)
                END
                FROM `{dataset_id}.subscriptions`
                WHERE cdu_id = '{cdu_code}'
            )"""

            # Build CREATE VIEW DDL with OPTIONS
            # Determine if this is a wrangle view
            is_wrangle = product_family == "Custom Wrangle"

            # Build description - keep it simple, avoid JSON-like syntax
            description_parts = []
            if is_wrangle:
                description_parts.append(f"Custom Wrangle for {org_info.company_name}")
            description_parts.append(f"dataset_legacy_id: {dataset_legacy_id}")
            description = " | ".join(description_parts)

            # Build labels - sanitize values for BigQuery
            # (lowercase, alphanumeric + hyphens only, no spaces)
            # BigQuery label requirements:
            # - Keys: lowercase letters, numbers, hyphens, underscores
            # - Values: lowercase letters, numbers, hyphens, underscores (no spaces!)
            sanitized_org_code = (
                org_info.org_code.lower().replace(" ", "_").replace("&", "and")
            )
            # Remove any other non-alphanumeric characters except hyphens and underscores
            sanitized_org_code = "".join(
                c for c in sanitized_org_code if c.isalnum() or c in "-_"
            )

            labels_list = [
                f'("cdu_code", "{cdu_code}")',
                f'("org_code", "{sanitized_org_code}")',
            ]
            labels_str = ", ".join(labels_list)

            create_view_ddl = f"""
            CREATE OR REPLACE VIEW `{view_id}`
            OPTIONS(
              description="{description}",
              labels=[{labels_str}]
            )
            AS
            SELECT DISTINCT {', '.join(fields)}
            FROM `{full_table_id}`
            WHERE {entitlement_clause}
            """

            if dry_run:
                print(f"[DRY RUN] Would create view: {view_name}")
            else:
                try:
                    create_job = client.query(create_view_ddl)
                    create_job.result()
                    created_views.append(view_name)
                    print(f"✓ Created view: {view_name}")

                    # Update view with field descriptions if available
                    if field_descriptions:
                        view = client.get_table(view_id)
                        updated_schema = []
                        for field in view.schema:
                            description = field_descriptions.get(field.name, "")
                            # Truncate description to 1000 chars and make ASCII-safe
                            if description:
                                description = (
                                    description[:1000]
                                    .encode("ascii", "ignore")
                                    .decode()
                                )
                            updated_schema.append(
                                bigquery.SchemaField(
                                    field.name,
                                    field.field_type,
                                    description=description,
                                )
                            )
                        view.schema = updated_schema
                        client.update_table(view, ["schema"])

                    # Step 2: Add to authorized views if this is a wrangle table
                    if is_wrangle:
                        try:
                            # Extract source dataset from full_table_id
                            # Format: crux-data-prd.wrangle_cx42749.TABLE_NAME
                            source_parts = full_table_id.split(".")
                            if len(source_parts) >= 2:
                                source_dataset_project = source_parts[0]
                                source_dataset_name = source_parts[1]
                                source_dataset_id = (
                                    f"{source_dataset_project}.{source_dataset_name}"
                                )

                                # Get current dataset to check/update authorized views
                                source_dataset = client.get_dataset(source_dataset_id)

                                # Build the view reference with FULL project ID
                                view_reference = {
                                    "projectId": client_project,
                                    "datasetId": client_dataset,
                                    "tableId": view_name,
                                }

                                # Check if already in authorized views
                                current_authorized_views = list(
                                    source_dataset.access_entries
                                )
                                view_access = bigquery.AccessEntry(
                                    role=None,
                                    entity_type="view",
                                    entity_id=view_reference,
                                )

                                # Only add if not already present
                                if view_access not in current_authorized_views:
                                    current_authorized_views.append(view_access)
                                    source_dataset.access_entries = (
                                        current_authorized_views
                                    )
                                    client.update_dataset(
                                        source_dataset, ["access_entries"]
                                    )
                                    print(
                                        f"  ✓ Added to authorized views in {source_dataset_id}"
                                    )
                                else:
                                    print(
                                        f"  ✓ Already in authorized views in {source_dataset_id}"
                                    )

                        except Exception as auth_err:
                            print(
                                f"  ⚠ Warning: Could not add to authorized views: {auth_err}"
                            )
                            # Don't fail the whole operation if this step fails

                except Exception as create_err:
                    error_msg = f"Error creating view {view_name}: {create_err}"
                    errors.append(error_msg)
                    print(f"✗ {error_msg}")

        # Step 10: Create utility views (catalog, schemas, validations) -
        # only if not provisioning a specific view
        if not view_name_to_provision:
            utility_views = {
                "catalog": f"""
                    CREATE OR REPLACE VIEW `{dataset_id}.catalog` AS
                    SELECT cat.* EXCEPT(table_dataset, table_name),
                           sub.type as subscription_type,
                           sub.service_type,
                           sub.validFrom,
                           sub.validTo
                    FROM `{source_project}.crux_platform.catalog` AS cat
                    JOIN `{dataset_id}.subscriptions` AS sub USING (cdu_id)
                    ORDER BY cat.view_name
                """,
                "schemas": f"""
                    CREATE OR REPLACE VIEW `{dataset_id}.schemas` AS
                    SELECT cs.*
                    FROM `{source_project}.crux_platform.crux_schema` AS cs
                    JOIN `{dataset_id}.subscriptions` AS sub USING (cdu_id)
                    ORDER BY cdu_id, table_name, ord
                """,
                "validations": f"""
                    CREATE OR REPLACE VIEW `{dataset_id}.validations` AS
                    SELECT * EXCEPT(org_id)
                    FROM `{source_project}.crux_platform.validations`
                    WHERE org_id = '{org_id}'
                """,
            }

            for util_view_name, util_ddl in utility_views.items():
                util_view_id = f"{dataset_id}.{util_view_name}"
                try:
                    client.get_table(util_view_id)
                    skipped_views.append(util_view_name)
                except Exception:
                    if dry_run:
                        print(f"[DRY RUN] Would create utility view: {util_view_name}")
                    else:
                        try:
                            create_job = client.query(util_ddl)
                            create_job.result()
                            created_views.append(util_view_name)
                            print(f"✓ Created utility view: {util_view_name}")
                        except Exception as util_err:
                            errors.append(
                                f"Error creating {util_view_name}: {util_err}"
                            )

        # Summary
        print("\n" + "=" * 60)
        print("Provisioning Summary:")
        print(f"Created: {len(created_views)} view(s)")
        print(f"Skipped (already exist): {len(skipped_views)} view(s)")
        print(f"Errors: {len(errors)}")

        if created_views:
            print("\nCreated views:")
            for view in created_views[:10]:
                print(f"  - {view}")
            if len(created_views) > 10:
                print(f"  ... and {len(created_views) - 10} more")

        if errors:
            print("\nErrors encountered:")
            for error in errors[:5]:
                print(f"  - {error}")
            if len(errors) > 5:
                print(f"  ... and {len(errors) - 5} more")

    def _provision_add_by_cdu(self, cdu_id: str, dry_run: bool = False):
        """Provision a specific CDU for all entitled organizations"""

        if not cdu_id:
            print("Error: CDU ID is required")
            raise typer.Exit(1)

        client = self.get_bigquery_client()

        # Find the CDU with case-insensitive search
        cdu_query = f"""
        SELECT id, code
        FROM `crux-data-lake.crux_federated.pa_cdu`
        WHERE UPPER(code) = UPPER('{cdu_id}')
        LIMIT 1
        """

        cdu_job = client.query(cdu_query)
        cdu_results = list(cdu_job.result())

        if not cdu_results:
            print(f"Error: CDU {cdu_id} not found")
            raise typer.Exit(1)

        actual_cdu_code = cdu_results[0].code
        print(
            f"{'[DRY RUN] ' if dry_run else ''}Provisioning CDU"
            f" {actual_cdu_code} for all entitled organizations"
        )
        print("=" * 60)

        # Get all organizations entitled to this CDU
        entitlements_query = f"""
        SELECT
            sr.external_id,
            pi.company_name
        FROM `crux-data-lake.crux_platform.service_restrictions` sr
        LEFT JOIN `crux-data-lake.crux_federated.pa_identity` pi
            ON sr.external_id = pi.external_id
        WHERE sr.cdu_id = '{actual_cdu_code}'
        ORDER BY sr.external_id
        """

        ent_job = client.query(entitlements_query)
        entitled_orgs = list(ent_job.result())

        if not entitled_orgs:
            print(f"No organizations are entitled to CDU {actual_cdu_code}")
            return

        print(f"Found {len(entitled_orgs)} entitled organization(s)\n")

        # Provision each organization
        success_count = 0
        error_count = 0

        for org in entitled_orgs:
            org_id = org.external_id
            org_name = org.company_name or "Unknown"

            print(f"\nProvisioning {org_id} ({org_name})...")
            try:
                self._provision_add_by_org(org_id, actual_cdu_code, dry_run)
                success_count += 1
            except Exception as e:
                print(f"  Error provisioning {org_id}: {str(e)[:100]}")
                error_count += 1

        # Final summary
        print("\n" + "=" * 60)
        print("CDU Provisioning Summary:")
        print(f"  CDU: {actual_cdu_code}")
        print(f"  Total Organizations: {len(entitled_orgs)}")
        print(f"  Successfully Provisioned: {success_count}")
        print(f"  Errors: {error_count}")

    def provision_delete(self, org_id, view_name):
        """Delete provisioning for an organization by removing views"""
        try:
            if not view_name:
                print("Error: View name is required for deletion")
                raise typer.Exit(1)

            client = self.get_bigquery_client()
            org_id_lower = org_id.lower()
            project_name = f"crux-client-{org_id_lower}"
            dataset_name = f"client_{org_id_lower}"

            if view_name != "all":
                # Delete specific view
                view_id = f"{project_name}.{dataset_name}.{view_name}"

                print(f"WARNING: This will delete view: {view_id}")
                confirmation = typer.prompt("Type 'DELETE' to confirm deletion")
                if confirmation != "DELETE":
                    print("Deletion cancelled")
                    raise typer.Exit(1)

                try:
                    client.delete_table(view_id)
                    print(f"Successfully deleted view: {view_id}")
                except Exception as e:
                    print(f"Error deleting view: {e}")

            else:
                # Delete entire dataset (all views)
                dataset_id = f"{project_name}.{dataset_name}"

                print(f"WARNING: This will delete ALL views in dataset: {dataset_id}")
                second_confirmation = typer.prompt(
                    "Type 'CONFIRM-ALL' to proceed with full deprovisioning"
                )
                if second_confirmation != "CONFIRM-ALL":
                    print("Full deprovisioning cancelled")
                    raise typer.Exit(1)

                try:
                    client.delete_dataset(
                        dataset_id, delete_contents=True, not_found_ok=True
                    )
                    print(f"Successfully deleted dataset: {dataset_id}")
                except Exception as e:
                    print(f"Error deleting dataset: {e}")

        except Exception as e:
            print(f"Error deleting provisioning: {e}")
            raise typer.Exit(1)

    def share_list(self, org_id):
        """List shares for an organization"""
        try:
            client = self.get_bigquery_client()
            org_id_lower = org_id.lower()
            project_name = f"crux-client-{org_id_lower}"
            dataset_name = f"client_{org_id_lower}"

            # Get dataset information
            datasets_query = f"""
            SELECT dataset_id, creation_time
            FROM `{project_name}.INFORMATION_SCHEMA.SCHEMATA`
            WHERE dataset_id = '{dataset_name}'
            """

            try:
                datasets_job = client.query(datasets_query)
                datasets = list(datasets_job.result())
            except Exception as e:
                print(f"Note: Could not access dataset information: {e}")
                datasets = []

            # Get views information
            views_query = f"""
            SELECT table_name, creation_time
            FROM `{project_name}.{dataset_name}.INFORMATION_SCHEMA.TABLES`
            WHERE table_type = 'VIEW'
            """

            try:
                views_job = client.query(views_query)
                views = list(views_job.result())
            except Exception as e:
                print(f"Note: Could not access views information: {e}")
                views = []

            print(f"Share Information for {org_id}:")

            if datasets:
                print("\nDataset:")
                headers = ["Dataset", "Created"]
                rows = []
                for dataset in datasets:
                    created = (
                        dataset.creation_time.strftime("%Y-%m-%d")
                        if dataset.creation_time
                        else "N/A"
                    )
                    rows.append([dataset.dataset_id, created])
                self.print_table(headers, rows)

            if views:
                print(f"\nViews ({len(views)}):")
                headers = ["View Name", "Created", "CDU"]
                rows = []
                for view in views:
                    created = (
                        view.creation_time.strftime("%Y-%m-%d")
                        if view.creation_time
                        else "N/A"
                    )
                    cdu = (
                        view.table_name.split("__")[0]
                        if "__" in view.table_name
                        else "N/A"
                    )
                    rows.append([view.table_name, created, cdu])
                self.print_table(headers, rows)

            if not datasets:
                print("No dataset found for this organization")

        except Exception as e:
            print(f"Error listing shares: {e}")
            raise typer.Exit(1)

    def share_add(self, org_id, view_name):
        """Add sharing for a specific view"""
        try:
            if not view_name:
                print("Error: View name is required")
                raise typer.Exit(1)

            org_id_lower = org_id.lower()
            view_id = f"crux-client-{org_id_lower}.client_{org_id_lower}.{view_name}"

            print(f"Adding share authorization for view: {view_id}")
            print("This would authorize the view to access source datasets")

            # Simplified implementation - in production, you'd need to:
            # 1. Determine which datasets this view needs access to
            # 2. Add the view to those datasets' authorized views list

            print(f"Share configuration initiated for view {view_name}")
            print("Note: Full share implementation requires source table determination")

        except Exception as e:
            print(f"Error adding share: {e}")
            raise typer.Exit(1)

    def share_delete(self, org_id, view_name):
        """Delete sharing for a specific view"""
        try:
            if not view_name:
                print("Error: View name is required")
                raise typer.Exit(1)

            org_id_lower = org_id.lower()
            view_id = f"crux-client-{org_id_lower}.client_{org_id_lower}.{view_name}"

            print(f"WARNING: This will remove share authorization for view: {view_id}")
            confirmation = typer.prompt("Type 'DELETE-SHARE' to confirm")
            if confirmation != "DELETE-SHARE":
                print("Share deletion cancelled")
                raise typer.Exit(1)

            print(f"Share removal initiated for view {view_name}")
            print(
                "Note: Full share deletion implementation requires source table determination"
            )

        except Exception as e:
            print(f"Error deleting share: {e}")
            raise typer.Exit(1)

    def provision_missing_views(
        self,
        org_id: Optional[str] = typer.Option(
            None, "--org_id", help="Organization ID (e.g., CXO12345)"
        ),
    ):
        """List missing views for a specific organization"""
        try:
            # Validate that org_id is provided
            if not org_id:
                print("Error: Organization ID is required")
                raise typer.Exit(1)

            # Validate org_id format
            if not org_id.startswith("CXO"):
                print("Error: Organization ID must start with 'CXO'")
                raise typer.Exit(1)

            if not org_id[3:].isdigit() or len(org_id) != 8:
                print(
                    "Error: Organization ID must be in format CXO followed by 5 digits"
                    " (e.g., CXO12345)"
                )
                raise typer.Exit(1)

            client = self.get_bigquery_client()

            print(f"Finding missing views for organization {org_id}")
            print("=" * 60)

            # Step 1: Get org project information
            org_project_query = f"""
            SELECT project, dataset, source_project
            FROM `crux-data-lake.crux_platform.org_projects`
            WHERE external_id = '{org_id}'
            LIMIT 1
            """

            proj_job = client.query(org_project_query)
            proj_results = list(proj_job.result())

            if not proj_results:
                print(f"Error: Organization {org_id} not found in org_projects")
                raise typer.Exit(1)

            proj_info = proj_results[0]
            client_project = proj_info.project
            client_dataset = proj_info.dataset
            source_project = proj_info.source_project

            print(f"Source: {source_project}")
            print(f"Target: {client_project}.{client_dataset}\n")

            # Step 2: Get entitled CDUs for this org
            entitled_cdus_query = f"""
            SELECT cdu_id
            FROM `crux-data-lake.crux_platform.service_restrictions`
            WHERE external_id = '{org_id}'
            ORDER BY cdu_id
            """

            entitled_job = client.query(entitled_cdus_query)
            entitled_cdus = [row.cdu_id for row in entitled_job.result()]

            if not entitled_cdus:
                print(f"Warning: Organization {org_id} has no entitlements")
                raise typer.Exit(1)

            print(f"Entitled CDUs: {', '.join(entitled_cdus)}\n")

            # Step 3: Get expected views from catalog
            cdu_in_list = "'" + "','".join(entitled_cdus) + "'"
            catalog_query = f"""
            SELECT view_name, cdu_id
            FROM `{source_project}.crux_platform.catalog`
            WHERE cdu_id IN ({cdu_in_list})
            ORDER BY cdu_id, view_name
            """

            catalog_job = client.query(catalog_query)
            expected_views = set()
            expected_by_cdu = {}

            for row in catalog_job.result():
                expected_views.add(row.view_name)
                if row.cdu_id not in expected_by_cdu:
                    expected_by_cdu[row.cdu_id] = []
                expected_by_cdu[row.cdu_id].append(row.view_name)

            # Add system views
            system_views = {"subscriptions", "catalog", "schemas", "validations"}
            expected_views |= system_views

            print(f"Expected views (from catalog): {len(expected_views)}\n")

            # Step 4: Get provisioned views from INFORMATION_SCHEMA
            provisioned_views = set()
            try:
                views_query = f"""
                SELECT table_name
                FROM `{client_project}.{client_dataset}.INFORMATION_SCHEMA.TABLES`
                WHERE table_type = 'VIEW'
                ORDER BY table_name
                """

                views_job = client.query(views_query)
                provisioned_views = set(row.table_name for row in views_job.result())

            except Exception as e:
                if "Not found" in str(e) or "does not exist" in str(e):
                    print(f"Dataset {client_dataset} does not exist yet")
                    provisioned_views = set()
                else:
                    raise

            print(f"Provisioned views: {len(provisioned_views)}\n")

            # Step 5: Calculate missing views
            missing_views = expected_views - provisioned_views
            orphaned_views = provisioned_views - expected_views

            # Step 6: Display results
            print("=" * 60)
            print("MISSING VIEWS (Expected but not provisioned):")
            print("=" * 60)

            if not missing_views:
                print("✓ All expected views are provisioned!\n")
            else:
                # Group by CDU
                missing_by_cdu = {}
                for view in missing_views:
                    if view in system_views:
                        cdu = "SYSTEM"
                    else:
                        cdu = view.split("__")[0] if "__" in view else "UNKNOWN"

                    if cdu not in missing_by_cdu:
                        missing_by_cdu[cdu] = []
                    missing_by_cdu[cdu].append(view)

                headers = ["CDU", "View Name", "Status"]
                rows = []

                for cdu in sorted(missing_by_cdu.keys()):
                    views = sorted(missing_by_cdu[cdu])
                    for i, view in enumerate(views):
                        if i == 0:
                            rows.append([cdu, view, "MISSING"])
                        else:
                            rows.append(["", view, "MISSING"])

                self.print_table(headers, rows)
                print(f"\nTotal missing views: {len(missing_views)}\n")

            # Step 7: Show orphaned views if any
            if orphaned_views:
                print("=" * 60)
                print("ORPHANED VIEWS (Provisioned but not in catalog):")
                print("=" * 60)

                headers = ["View Name", "Status"]
                rows = [[view, "ORPHANED"] for view in sorted(orphaned_views)]
                self.print_table(headers, rows)
                print(f"\nTotal orphaned views: {len(orphaned_views)}\n")

            # Step 8: Summary
            print("=" * 60)
            print("SUMMARY:")
            print("=" * 60)
            print(f"Expected views:    {len(expected_views)}")
            print(f"Provisioned views: {len(provisioned_views)}")
            print(f"Missing views:     {len(missing_views)}")
            print(f"Orphaned views:    {len(orphaned_views)}")
            print(f"Gap:               {len(missing_views)} views to provision")

            if missing_views:
                print("\nTo provision missing views, run:")
                print(
                    "  cruxctl bigsnow --instance bigquery orgs provision-add --org_id {org_id}"
                )

        except typer.Exit:
            raise
        except Exception as e:
            print(f"Error finding missing views: {e}")
            raise typer.Exit(1)
