import os
import json
import subprocess
import tempfile
import shutil
import time
import requests
import msal
import base64

def _load_env_creds(env_path: str = ".env"):
    """Internal helper to load Fabric credentials from .env into a dict raising on missing values."""
    if not os.path.exists(env_path):
        raise Exception(".env file not found. Run 'loomaa init' and configure authentication.")
    creds = {}
    with open(env_path, 'r', encoding='utf-8') as f:
        for line in f:
            if '=' in line:
                k, v = line.strip().split('=', 1)
                creds[k] = v.strip('\"\'')
    required = ["FABRIC_TENANT_ID", "FABRIC_CLIENT_ID", "FABRIC_CLIENT_SECRET", "FABRIC_WORKSPACE_ID"]
    missing = [k for k in required if not creds.get(k)]
    if missing:
        raise Exception(f"Missing required auth variables in .env: {', '.join(missing)}")
    return creds

def deploy_monolithic_tmdl(model_name: str, tmdl_path: str, delete_first: bool = False):
    """Deploy or update a semantic model using a single monolithic TMDL file.

    New strategy (fixes 400 MissingDefinition):
      - If model does NOT exist: POST create WITH embedded definition (no shell phase).
      - If model exists and delete_first (--clean) requested: delete then POST create WITH definition.
      - If model exists and not deleting: PUT updated definition in place (non-destructive, stable ID).

    This replaces the legacy two-phase shell-create + definition PUT which now returns MissingDefinition.
    """
    if not os.path.exists(tmdl_path):
        raise Exception(f"Monolithic TMDL file not found: {tmdl_path}")

    print(f"[loomaa] 🚀 Deploying monolithic TMDL for '{model_name}' -> {tmdl_path}")
    creds = _load_env_creds()
    access_token = get_fabric_access_token(creds)
    workspace_id = creds['FABRIC_WORKSPACE_ID']

    # Read & encode content once
    with open(tmdl_path, 'r', encoding='utf-8') as f:
        tmdl_content = f.read()
    tmdl_b64 = base64.b64encode(tmdl_content.encode('utf-8')).decode('utf-8')

    # Try to locate a companion definition.pbism generated by the modular compile
    pbism_source_path = os.path.join("compiled", f"{model_name}.SemanticModel", "definition.pbism")
    if os.path.exists(pbism_source_path):
        try:
            with open(pbism_source_path, 'r', encoding='utf-8') as f:
                pbism_content = f.read()
        except Exception:
            pbism_content = '{"version": "1.0", "artifacts": []}'
    else:
        # Fabric expects a definition.pbism artifact; provide minimal stub if not present
        pbism_content = '{"version": "1.0", "artifacts": []}'
    pbism_b64 = base64.b64encode(pbism_content.encode('utf-8')).decode('utf-8')

    headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
    base_url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/semanticmodels"

    existing_id = find_existing_semantic_model(access_token, workspace_id, model_name)

    # Delete first if requested
    if existing_id and delete_first:
        print(f"[loomaa] --clean specified. Deleting existing model id={existing_id} ...")
        delete_semantic_model(access_token, workspace_id, existing_id)
        time.sleep(2)
        existing_id = None

    # CREATE path (model does not exist)
    if not existing_id:
        # Primary expected path: files must reside under 'definition/' per Fabric error message
        part_paths_try = [
            "definition/model.tmdl",  # preferred / documented
            "model.tmdl"               # fallback
        ]
        resp = None
        last_error = None
        model_id = None
        for part_path in part_paths_try:
            create_parts = [
                {
                    "path": part_path,
                    "payloadType": "InlineBase64",
                    "payload": tmdl_b64
                },
                {
                    "path": "definition.pbism",
                    "payloadType": "InlineBase64",
                    "payload": pbism_b64
                }
            ]
            create_payload = {
                "displayName": model_name,
                "definition": {
                    "format": "TMDL",
                    "parts": create_parts
                }
            }
            resp = requests.post(base_url, json=create_payload, headers=headers)
            if resp.status_code in (200, 201, 202):
                break
            else:
                last_error = f"{resp.status_code} - {resp.text}"
                try:
                    err_code = resp.json().get("errorCode")
                except Exception:
                    err_code = None
                if err_code != "MissingDefinitionParts":
                    break
        if resp is None or resp.status_code not in (200, 201, 202):
            raise Exception(f"Create-with-definition failed after trying paths {part_paths_try}: {last_error}")
        model_id = None
        try:
            data = resp.json() if resp.text else {}
            model_id = data.get('id')
        except Exception:
            data = {}
        print(f"[loomaa] ✅ Create accepted (status {resp.status_code}) id={model_id or 'unknown'}")
        op_url = resp.headers.get('Location') or resp.headers.get('Operation-Location')
        if op_url:
            retry_after = int(resp.headers.get('Retry-After', 15))
            print("[loomaa] ⏳ Polling create operation...")
            op_result = wait_for_operation_completion(op_url, access_token, retry_after)
            status = op_result.get('status')
            if status != 'Succeeded':
                print(f"[loomaa] ⚠️ Create operation ended with status {status}")
            return {"status": status, "id": model_id, "operationResult": op_result}
        return {"status": "Created", "id": model_id}

    # UPDATE path (model exists, not deleting)
    model_id = existing_id
    print(f"[loomaa] Updating existing model id={model_id} (non-destructive)")
    def_url = f"{base_url}/{model_id}/definitions?format=TMDL"
    definition_payload = {
        "format": "TMDL",
        "parts": [
            {
                "path": "definition/model.tmdl",
                "payloadType": "InlineBase64",
                "payload": tmdl_b64
            },
            {
                "path": "definition.pbism",
                "payloadType": "InlineBase64",
                "payload": pbism_b64
            }
        ]
    }
    resp = requests.put(def_url, json=definition_payload, headers=headers)
    if resp.status_code in (200, 201):
        print("[loomaa] ✅ Definition updated successfully")
        return {"status": "Succeeded", "id": model_id}
    if resp.status_code == 202:
        op_url = resp.headers.get('Location') or resp.headers.get('Operation-Location')
        retry_after = int(resp.headers.get('Retry-After', 15))
        if not op_url:
            print("[loomaa] Accepted (202) without operation location; update will finalize asynchronously")
            return {"status": "Accepted", "id": model_id}
        print("[loomaa] ⏳ Polling update operation...")
        op_result = wait_for_operation_completion(op_url, access_token, retry_after)
        status = op_result.get('status')
        if status == 'Succeeded':
            print("[loomaa] ✅ Monolithic update completed successfully")
        else:
            print(f"[loomaa] ⚠️ Update operation ended with status {status}")
        return {"status": status, "id": model_id, "operationResult": op_result}

    # Failure
    try:
        err_json = resp.json()
    except Exception:
        err_json = resp.text
    raise Exception(f"Definition update failed: {resp.status_code} - {err_json}")

def deploy_complete_semantic_model(model_name, semantic_model_path):
    """
    Deploy semantic model using complete .SemanticModel structure.
    Uses the Fabric REST API with all required files (definition.pbism, model.tmdl, .platform).
    """
    env_path = ".env"
    if not os.path.exists(env_path):
        print(f"[loomaa] .env file not found for model '{model_name}'.")
        return
    
    # Load credentials from .env
    creds = {}
    with open(env_path, 'r', encoding='utf-8') as f:
        for line in f:
            if '=' in line:
                k, v = line.strip().split('=', 1)
                creds[k] = v.strip('"\'')
    
    required = ["FABRIC_TENANT_ID", "FABRIC_CLIENT_ID", "FABRIC_CLIENT_SECRET", "FABRIC_WORKSPACE_ID"]
    missing = [k for k in required if not creds.get(k)]
    if missing:
        raise Exception(f"Missing required auth variables in .env: {', '.join(missing)}")
    
    # Validate structure
    definition_path = os.path.join(semantic_model_path, "definition", "model.tmdl")
    pbism_path = os.path.join(semantic_model_path, "definition.pbism")
    platform_path = os.path.join(semantic_model_path, ".platform")
    
    if not os.path.exists(definition_path):
        raise Exception(f"Missing definition/model.tmdl in {semantic_model_path}")
    if not os.path.exists(pbism_path):
        raise Exception(f"Missing definition.pbism in {semantic_model_path}")
    
    print(f"[loomaa] 🚀 Deploying complete semantic model '{model_name}' using Fabric REST API...")
    
    try:
        # Get access token
        print(f"[loomaa] Authenticating with Azure...")
        access_token = get_fabric_access_token(creds)
        
        # Check for existing model and delete if found
        print(f"[loomaa] Checking for existing model...")
        existing_model_id = find_existing_semantic_model(access_token, creds["FABRIC_WORKSPACE_ID"], model_name)
        if existing_model_id:
            print(f"[loomaa] Found existing model '{model_name}'. Deleting to avoid duplicates...")
            delete_semantic_model(access_token, creds["FABRIC_WORKSPACE_ID"], existing_model_id)
            print(f"[loomaa] ✅ Existing model deleted successfully")
        
        # Read all required files
        print(f"[loomaa] Reading semantic model files...")
        
        # Collect all TMDL files from the modular structure
        definition_dir = os.path.join(semantic_model_path, "definition")
        payload_parts = []
        
        # 1. Read main model.tmdl (metadata)
        model_tmdl_path = os.path.join(definition_dir, "model.tmdl")
        if os.path.exists(model_tmdl_path):
            with open(model_tmdl_path, 'r', encoding='utf-8') as f:
                tmdl_content = f.read()
            tmdl_base64 = base64.b64encode(tmdl_content.encode('utf-8')).decode('utf-8')
            payload_parts.append({
                "path": "definition/model.tmdl",
                "payload": tmdl_base64,
                "payloadType": "InlineBase64"
            })
        
        # 2. Read database.tmdl
        database_tmdl_path = os.path.join(definition_dir, "database.tmdl")
        if os.path.exists(database_tmdl_path):
            with open(database_tmdl_path, 'r', encoding='utf-8') as f:
                database_content = f.read()
            database_base64 = base64.b64encode(database_content.encode('utf-8')).decode('utf-8')
            payload_parts.append({
                "path": "definition/database.tmdl",
                "payload": database_base64,
                "payloadType": "InlineBase64"
            })

        # 2b. Read expressions.tmdl (needed for DirectLake expressionSource)
        expressions_tmdl_path = os.path.join(definition_dir, "expressions.tmdl")
        if os.path.exists(expressions_tmdl_path):
            with open(expressions_tmdl_path, 'r', encoding='utf-8') as f:
                expr_content = f.read()
            expr_base64 = base64.b64encode(expr_content.encode('utf-8')).decode('utf-8')
            payload_parts.append({
                "path": "definition/expressions.tmdl",
                "payload": expr_base64,
                "payloadType": "InlineBase64"
            })
        
        # 3. Read all table files from tables/ directory
        tables_dir = os.path.join(definition_dir, "tables")
        if os.path.exists(tables_dir):
            for table_file in os.listdir(tables_dir):
                if table_file.endswith('.tmdl'):
                    table_path = os.path.join(tables_dir, table_file)
                    with open(table_path, 'r', encoding='utf-8') as f:
                        table_content = f.read()
                    table_base64 = base64.b64encode(table_content.encode('utf-8')).decode('utf-8')
                    payload_parts.append({
                        "path": f"definition/tables/{table_file}",
                        "payload": table_base64,
                        "payloadType": "InlineBase64"
                    })
        
        # 4. Read relationships.tmdl if exists
        relationships_path = os.path.join(definition_dir, "relationships.tmdl")
        if os.path.exists(relationships_path):
            with open(relationships_path, 'r', encoding='utf-8') as f:
                rel_content = f.read()
            rel_base64 = base64.b64encode(rel_content.encode('utf-8')).decode('utf-8')
            payload_parts.append({
                "path": "definition/relationships.tmdl",
                "payload": rel_base64,
                "payloadType": "InlineBase64"
            })
        
        # 5. Read cultures/en-US.tmdl if exists
        cultures_dir = os.path.join(definition_dir, "cultures")
        if os.path.exists(cultures_dir):
            for culture_file in os.listdir(cultures_dir):
                if culture_file.endswith('.tmdl'):
                    culture_path = os.path.join(cultures_dir, culture_file)
                    with open(culture_path, 'r', encoding='utf-8') as f:
                        culture_content = f.read()
                    culture_base64 = base64.b64encode(culture_content.encode('utf-8')).decode('utf-8')
                    payload_parts.append({
                        "path": f"definition/cultures/{culture_file}",
                        "payload": culture_base64,
                        "payloadType": "InlineBase64"
                    })

        # 5b. Read roles/*.tmdl if exists
        roles_dir = os.path.join(definition_dir, "roles")
        if os.path.exists(roles_dir):
            for role_file in os.listdir(roles_dir):
                if role_file.endswith('.tmdl'):
                    role_path = os.path.join(roles_dir, role_file)
                    with open(role_path, 'r', encoding='utf-8') as f:
                        role_content = f.read()
                    role_base64 = base64.b64encode(role_content.encode('utf-8')).decode('utf-8')
                    payload_parts.append({
                        "path": f"definition/roles/{role_file}",
                        "payload": role_base64,
                        "payloadType": "InlineBase64"
                    })
        
        # 6. Read definition.pbism
        with open(pbism_path, 'r', encoding='utf-8') as f:
            pbism_content = f.read()
        pbism_base64 = base64.b64encode(pbism_content.encode('utf-8')).decode('utf-8')
        payload_parts.append({
            "path": "definition.pbism",
            "payload": pbism_base64,
            "payloadType": "InlineBase64"
        })
        
        # 7. Add .platform file if it exists
        if os.path.exists(platform_path):
            with open(platform_path, 'r', encoding='utf-8') as f:
                platform_content = f.read()
            platform_base64 = base64.b64encode(platform_content.encode('utf-8')).decode('utf-8')
            payload_parts.append({
                "path": ".platform",
                "payload": platform_base64,
                "payloadType": "InlineBase64"
            })
        
        # Create complete payload with all files
        payload = {
            "displayName": f"{model_name}",
            "definition": {
                "parts": payload_parts
            }
        }
        
        print(f"[loomaa] Collected {len(payload_parts)} files for deployment")
        
        # Deploy using Fabric API
        print(f"[loomaa] Deploying to Fabric workspace...")
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        }
        
        workspace_id = creds['FABRIC_WORKSPACE_ID']
        url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/semanticmodels"
        
        print(f"[loomaa] Deploying to Fabric workspace: {workspace_id}")
        response = requests.post(url, json=payload, headers=headers)
        
        print(f"[loomaa] API Response Status: {response.status_code}")
        
        if response.status_code in [200, 201]:
            # Synchronous success
            try:
                result = response.json() if response.text.strip() else {}
                print(f"[loomaa] ✅ Semantic model '{model_name}' deployed successfully!")
                model_id = result.get('id', 'Unknown') if result else 'Unknown'
                print(f"[loomaa] Model ID: {model_id}")
                return result
            except json.JSONDecodeError:
                print(f"[loomaa] ✅ Semantic model '{model_name}' deployed successfully!")
                print(f"[loomaa] Model ID: Unknown (empty response)")
                return {"status": "success"}
                
        elif response.status_code == 202:
            # Asynchronous operation - need to poll for completion
            operation_url = response.headers.get('Location')
            retry_after = int(response.headers.get('Retry-After', 20))
            
            if operation_url:
                print(f"[loomaa] ⏳ Deployment started (asynchronous). Checking status...")
                result = wait_for_operation_completion(operation_url, access_token, retry_after)
                if result.get('status') == 'Succeeded':
                    print(f"[loomaa] ✅ Semantic model '{model_name}' deployed successfully!")
                    print(f"[loomaa] Model ID: {result.get('resourceId', 'Unknown')}")
                    return result
                else:
                    raise Exception(f"Deployment operation failed: {result}")
            else:
                print(f"[loomaa] ✅ Semantic model '{model_name}' deployment accepted!")
                print(f"[loomaa] Note: Deployment may take a few minutes to appear in workspace")
                return {"status": "accepted"}
        else:
            error_details = ""
            try:
                error_data = response.json()
                error_details = f" - {error_data}"
            except:
                error_details = f" - {response.text}"
            
            raise Exception(f"Fabric API deployment failed: {response.status_code}{error_details}")
        
    except Exception as e:
        print(f"[loomaa] ❌ Deployment failed: {e}")
        raise

def deploy_model_fabric_api(model_name, tmdl_path):
    """
    Deploy semantic model using Microsoft Fabric REST API.
    
    This is the revolutionary solution that provides full TMDL deployment
    without PowerShell dependencies - perfect for local development!
    """
    env_path = ".env"
    if not os.path.exists(env_path):
        print(f"[loomaa] .env file not found for model '{model_name}'.")
        return
    
    # Load credentials from .env
    creds = {}
    with open(env_path, 'r', encoding='utf-8') as f:
        for line in f:
            if '=' in line:
                k, v = line.strip().split('=', 1)
                creds[k] = v.strip('"\'')
    
    required = ["FABRIC_TENANT_ID", "FABRIC_CLIENT_ID", "FABRIC_CLIENT_SECRET", "FABRIC_WORKSPACE_ID"]
    missing = [k for k in required if not creds.get(k)]
    if missing:
        raise Exception(f"Missing required auth variables in .env: {', '.join(missing)}")
    
    if not os.path.exists(tmdl_path):
        raise Exception(f"TMDL file not found: {tmdl_path}")
    
    print(f"[loomaa] 🚀 Deploying model '{model_name}' using Fabric REST API...")
    
    try:
        # Step 1: Get access token
        print(f"[loomaa] Authenticating with Azure...")
        access_token = get_fabric_access_token(creds)
        
        # Step 2: Read and encode TMDL content
        print(f"[loomaa] Reading TMDL content...")
        with open(tmdl_path, 'r', encoding='utf-8') as f:
            tmdl_content = f.read()
        
        # Encode as base64 for Fabric API
        tmdl_base64 = base64.b64encode(tmdl_content.encode('utf-8')).decode('utf-8')
        
        # Create definition.pbism content (required by Fabric API)
        pbism_content = {
            "version": "1.0",
            "artifacts": [
                {
                    "report": {
                        "path": f"{model_name}.Report"
                    }
                }
            ]
        }
        pbism_json = json.dumps(pbism_content, indent=2)
        pbism_base64 = base64.b64encode(pbism_json.encode('utf-8')).decode('utf-8')
        
        # Step 3: Create semantic model payload with both required files
        payload = {
            "displayName": f"{model_name} (Loomaa)",
            "definition": {
                "parts": [
                    {
                        "path": "definition/model.tmdl",
                        "payload": tmdl_base64,
                        "payloadType": "InlineBase64"
                    },
                    {
                        "path": "definition.pbism",
                        "payload": pbism_base64,
                        "payloadType": "InlineBase64"
                    }
                ]
            }
        }
        
        # Step 4: Deploy using Fabric API
        print(f"[loomaa] Deploying to Fabric workspace...")
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        }
        
        workspace_id = creds['FABRIC_WORKSPACE_ID']
        url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/semanticmodels"
        
        print(f"[loomaa] Deploying to Fabric workspace: {workspace_id}")
        response = requests.post(url, json=payload, headers=headers)
        
        print(f"[loomaa] API Response Status: {response.status_code}")
        
        if response.status_code in [200, 201]:
            # Synchronous success
            try:
                result = response.json() if response.text.strip() else {}
                print(f"[loomaa] ✅ Semantic model '{model_name}' deployed successfully!")
                model_id = result.get('id', 'Unknown') if result else 'Unknown'
                print(f"[loomaa] Model ID: {model_id}")
                return result
            except json.JSONDecodeError:
                print(f"[loomaa] ✅ Semantic model '{model_name}' deployed successfully!")
                print(f"[loomaa] Model ID: Unknown (empty response)")
                return {"status": "success"}
                
        elif response.status_code == 202:
            # Asynchronous operation - need to poll for completion
            operation_url = response.headers.get('Location')
            retry_after = int(response.headers.get('Retry-After', 20))
            
            if operation_url:
                print(f"[loomaa] ⏳ Deployment started (asynchronous). Checking status...")
                result = wait_for_operation_completion(operation_url, access_token, retry_after)
                if result.get('status') == 'Succeeded':
                    print(f"[loomaa] ✅ Semantic model '{model_name}' deployed successfully!")
                    print(f"[loomaa] Model ID: {result.get('resourceId', 'Unknown')}")
                    return result
                else:
                    raise Exception(f"Deployment operation failed: {result}")
            else:
                print(f"[loomaa] ✅ Semantic model '{model_name}' deployment accepted!")
                print(f"[loomaa] Note: Deployment may take a few minutes to appear in workspace")
                return {"status": "accepted"}
        else:
            error_details = ""
            try:
                error_data = response.json()
                error_details = f" - {error_data}"
            except:
                error_details = f" - {response.text}"
            
            raise Exception(f"Fabric API deployment failed: {response.status_code}{error_details}")
        
    except Exception as e:
        print(f"[loomaa] ❌ Deployment failed: {e}")
        raise

def get_fabric_access_token(creds):
    """Get access token for Microsoft Fabric API using service principal."""
    
    app = msal.ConfidentialClientApplication(
        client_id=creds['FABRIC_CLIENT_ID'],
        client_credential=creds['FABRIC_CLIENT_SECRET'],
        authority=f"https://login.microsoftonline.com/{creds['FABRIC_TENANT_ID']}"
    )
    
    # Request token with Fabric scope
    result = app.acquire_token_for_client(scopes=["https://api.fabric.microsoft.com/.default"])
    
    if "access_token" not in result:
        error_msg = result.get("error_description", "Unknown authentication error")
        raise Exception(f"Failed to get access token: {error_msg}")
    
    return result["access_token"]

def wait_for_operation_completion(operation_url, access_token, initial_delay=20):
    """Wait for Fabric async operation to complete and return the result."""
    import time
    
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }
    
    # Initial wait as suggested by API
    print(f"[loomaa] Waiting {initial_delay} seconds for operation to process...")
    time.sleep(initial_delay)
    
    # Poll for completion (max 5 minutes)
    max_attempts = 15  # 15 attempts * 20 seconds = 5 minutes max
    attempt = 0
    
    while attempt < max_attempts:
        try:
            response = requests.get(operation_url, headers=headers)
            
            if response.status_code == 200:
                operation_result = response.json()
                status = operation_result.get('status', 'Unknown')
                
                print(f"[loomaa] Operation status: {status}")
                
                if status == 'Succeeded':
                    return operation_result
                elif status == 'Failed':
                    error_details = operation_result.get('error', {})
                    raise Exception(f"Operation failed: {error_details}")
                elif status in ['Running', 'NotStarted']:
                    # Continue polling
                    time.sleep(20)
                    attempt += 1
                else:
                    # Unknown status, continue polling
                    time.sleep(20)
                    attempt += 1
            else:
                print(f"[loomaa] Warning: Cannot check operation status (HTTP {response.status_code})")
                time.sleep(20)
                attempt += 1
                
        except Exception as e:
            print(f"[loomaa] Warning: Error checking operation status: {e}")
            time.sleep(20)
            attempt += 1
    
    # Timeout reached
    print(f"[loomaa] ⚠️  Operation timeout reached. Deployment may still be processing in background.")
    return {"status": "timeout", "message": "Check workspace manually in a few minutes"}

def deploy_semantic_model(model_name, semantic_model_path):
    """
    Deploy a semantic model using the .SemanticModel structure created at compile time.
    
    This function expects the .SemanticModel folder structure that's already created
    by the compiler, including definition/, definition.pbism, and .platform files.
    """
    import os
    import subprocess
    
    env_path = ".env"
    if not os.path.exists(env_path):
        print(f"[loomaa] .env file not found for model '{model_name}'.")
        return
    
    # Load credentials from global .env
    creds = {}
    with open(env_path, 'r', encoding='utf-8') as f:
        for line in f:
            if '=' in line:
                k, v = line.strip().split('=', 1)
                creds[k] = v
    
    required = ["FABRIC_TENANT_ID", "FABRIC_CLIENT_ID", "FABRIC_CLIENT_SECRET", "FABRIC_WORKSPACE_ID"]
    missing = [k for k in required if not creds.get(k)]
    if missing:
        raise Exception(f"Missing required auth variables in .env: {', '.join(missing)}")
    
    if not os.path.exists(semantic_model_path):
        raise Exception(f"SemanticModel folder not found: {semantic_model_path}")
    
    # Verify structure
    definition_path = os.path.join(semantic_model_path, "definition", "model.tmdl")
    if not os.path.exists(definition_path):
        raise Exception(f"Invalid .SemanticModel structure: missing definition/model.tmdl in {semantic_model_path}")
    
    print(f"[loomaa] Deploying model '{model_name}' from .SemanticModel structure...")
    
    # Step 1: Ensure PowerShell 7 is available (auto-install if needed)
    pwsh_path = ensure_powershell7()
    
    try:
        # Step 2: Deploy directly using the existing .SemanticModel structure
        deploy_with_powershell(pwsh_path, model_name, semantic_model_path, creds)
        print(f"[loomaa] ✅ Model '{model_name}' deployed successfully!")
        
    except Exception as e:
        print(f"[loomaa] ❌ Deployment failed: {e}")
        raise

def deploy_single_model(model_name, tmdl_path):
    """
    Deploy a single model to Power BI Service with full TMDL support.
    
    Auto-installs dependencies and handles everything behind the scenes.
    User just runs 'loomaa deploy' and it works.
    """
    import os
    import requests
    import msal
    import json
    import subprocess
    import tempfile
    import shutil
    import time
    
    env_path = ".env"
    if not os.path.exists(env_path):
        print(f"[loomaa] .env file not found for model '{model_name}'.")
        return
    
    # Load credentials from global .env
    creds = {}
    with open(env_path, 'r', encoding='utf-8') as f:
        for line in f:
            if '=' in line:
                k, v = line.strip().split('=', 1)
                creds[k] = v
    
    required = ["FABRIC_TENANT_ID", "FABRIC_CLIENT_ID", "FABRIC_CLIENT_SECRET", "FABRIC_WORKSPACE_ID"]
    missing = [k for k in required if not creds.get(k)]
    if missing:
        raise Exception(f"Missing required auth variables in .env: {', '.join(missing)}")
    
    if not os.path.exists(tmdl_path):
        raise Exception(f"TMDL file not found: {tmdl_path}")
    
    print(f"[loomaa] Deploying model '{model_name}' to Power BI Service...")
    
    # Step 1: Ensure PowerShell 7 is available (auto-install if needed)
    pwsh_path = ensure_powershell7()
    
    # Step 2: Create .SemanticModel structure from TMDL
    pbip_folder = create_semantic_model_structure(model_name, tmdl_path)
    
    try:
        # Step 3: Deploy using PowerShell with all modules auto-installed
        deploy_with_powershell(pwsh_path, model_name, pbip_folder, creds)
        print(f"[loomaa] ✅ Model '{model_name}' deployed successfully!")
        
    finally:
        # Clean up temporary .SemanticModel folder
        if os.path.exists(pbip_folder):
            shutil.rmtree(pbip_folder)

def ensure_powershell7():
    """
    Ensure PowerShell 7 is available for deployment.
    
    For CI/CD environments (Azure DevOps, GitHub Actions): PowerShell 7 is pre-installed
    For local development: Provide clear instructions for manual installation
    """
    
    # First, check if pwsh is already available
    try:
        result = subprocess.run(["pwsh", "-Version"], capture_output=True, text=True, timeout=10)
        if result.returncode == 0:
            print("[loomaa] ✅ PowerShell 7 found")
            return "pwsh"
    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass
    
    # Check common installation paths
    common_paths = [
        r"C:\Program Files\PowerShell\7\pwsh.exe",
        r"C:\Program Files (x86)\PowerShell\7\pwsh.exe",
        os.path.expanduser(r"~\AppData\Local\Microsoft\powershell\pwsh.exe")
    ]
    
    for path in common_paths:
        if os.path.exists(path):
            print(f"[loomaa] ✅ PowerShell 7 found at: {path}")
            return path
    
    # PowerShell 7 not found - provide helpful instructions
    print("\n[loomaa] ❌ PowerShell 7 required for deployment but not found.")
    print("\n📖 Installation Instructions:")
    print("   Local Development:")
    print("   • Download from: https://github.com/PowerShell/PowerShell/releases")
    print("   • Or use winget: winget install Microsoft.PowerShell")
    print("   • Or use Chocolatey: choco install powershell-core")
    print("\n   CI/CD Environments:")
    print("   • Azure DevOps: PowerShell 7 is pre-installed (use pwsh: true)")
    print("   • GitHub Actions: PowerShell 7 is pre-installed")
    print("\n💡 After installation, restart your terminal and try again.")
    print("\n📚 Documentation: https://loomaa.dev/deployment")
    
    raise Exception("PowerShell 7 is required for deployment. See installation instructions above.")

def create_semantic_model_structure(model_name, tmdl_path):
    """Create proper .SemanticModel folder structure from TMDL file."""
    
    pbip_folder = os.path.join("compiled", f"{model_name}.SemanticModel")
    
    # Clean up any existing folder
    if os.path.exists(pbip_folder):
        shutil.rmtree(pbip_folder)
    
    # Create structure
    os.makedirs(pbip_folder)
    definition_folder = os.path.join(pbip_folder, "definition")
    os.makedirs(definition_folder)
    
    # Copy TMDL file to proper location
    tmdl_dest = os.path.join(definition_folder, "model.tmdl")
    shutil.copy2(tmdl_path, tmdl_dest)
    
    # Create .pbism metadata file
    pbism_content = {
        "version": "1.0",
        "artifacts": [
            {
                "report": {
                    "path": f"{model_name}.Report"
                }
            }
        ]
    }
    
    pbism_file = os.path.join(pbip_folder, "definition.pbism")
    with open(pbism_file, 'w') as f:
        json.dump(pbism_content, f, indent=2)
    
    print(f"[loomaa] ✅ Created .SemanticModel structure")
    return pbip_folder

def deploy_with_powershell(pwsh_path, model_name, pbip_folder, creds):
    """Deploy using PowerShell with auto module installation - matches Azure DevOps pipeline exactly."""
    
    ps_script = f"""
    $ErrorActionPreference = "Stop"
    $VerbosePreference = "Continue"
    
    Write-Host "[loomaa] Setting up PowerShell modules..."
    
    # Create temp directory for modules (like Azure DevOps pipeline)
    $workingFolder = Join-Path $env:TEMP "loomaa-modules"
    $moduleDir = Join-Path $workingFolder "modules"
    New-Item -ItemType Directory -Path $moduleDir -Force -ErrorAction SilentlyContinue | Out-Null
    
    # Download FabricPS-PBIP directly from GitHub (exactly like pipeline)
    $fabricModules = @(
        "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psm1",
        "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/pbidevmode/fabricps-pbip/FabricPS-PBIP.psd1"
    )
    
    foreach ($url in $fabricModules) {{
        $fileName = Split-Path $url -Leaf
        $filePath = Join-Path $moduleDir $fileName
        Write-Host "[loomaa] Downloading $fileName..."
        try {{
            Invoke-WebRequest -Uri $url -OutFile $filePath -UseBasicParsing
        }} catch {{
            Write-Error "Failed to download $fileName`: $_"
            exit 1
        }}
    }}
    
    # Install required modules (exactly like pipeline)
    Write-Host "[loomaa] Installing required modules..."
    Install-Module -Name Az.Accounts -Force -Scope CurrentUser -AllowClobber
    Install-Module -Name MicrosoftPowerBIMgmt -Force -Scope CurrentUser -AllowClobber
    
    # Import modules (exactly like pipeline)
    Write-Host "[loomaa] Importing modules..."
    Import-Module Az.Accounts -Force
    Import-Module MicrosoftPowerBIMgmt -Force
    Import-Module (Join-Path $moduleDir "FabricPS-PBIP.psm1") -Force
    
    Write-Host "[loomaa] ✅ Modules ready"
    
    # Authenticate (exactly like pipeline)
    Write-Host "[loomaa] Authenticating..."
    try {{
        Set-FabricAuthToken -servicePrincipalId "{creds['FABRIC_CLIENT_ID']}" -servicePrincipalSecret "{creds['FABRIC_CLIENT_SECRET']}" -tenantId "{creds['FABRIC_TENANT_ID']}" -reset
        
        $securePassword = ConvertTo-SecureString "{creds['FABRIC_CLIENT_SECRET']}" -AsPlainText -Force
        $credential = New-Object System.Management.Automation.PSCredential ("{creds['FABRIC_CLIENT_ID']}", $securePassword)
        Connect-PowerBIServiceAccount -ServicePrincipal -Credential $credential -TenantId "{creds['FABRIC_TENANT_ID']}"
        
        Write-Host "[loomaa] ✅ Authentication successful"
    }} catch {{
        Write-Error "Authentication failed: $_"
        exit 1
    }}
    
    # Deploy semantic model (exactly like pipeline)
    Write-Host "[loomaa] Deploying semantic model..."
    try {{
        $workspaceId = "{creds['FABRIC_WORKSPACE_ID']}"
        $semanticModelPath = "{os.path.abspath(pbip_folder)}"
        
        $result = Import-FabricItem -workspaceId $workspaceId -path $semanticModelPath
        
        if ($result) {{
            Write-Host "[loomaa] ✅ Deployment successful!"
            Write-Host "[loomaa] Dataset ID: $($result.Id)"
        }} else {{
            Write-Error "Deployment returned null result"
            exit 1
        }}
    }} catch {{
        Write-Error "Deployment failed: $_"
        exit 1
    }}
    """
    
    # Write and execute PowerShell script
    with tempfile.NamedTemporaryFile(mode='w', suffix='.ps1', delete=False, encoding='utf-8') as f:
        f.write(ps_script)
        ps_script_path = f.name
    
    try:
        print(f"[loomaa] Executing deployment...")
        result = subprocess.run([
            pwsh_path, "-ExecutionPolicy", "Bypass", "-File", ps_script_path
        ], capture_output=True, text=True, cwd=os.getcwd())
        
        if result.returncode == 0:
            # Print PowerShell output (filtered for user-friendly messages)
            output_lines = result.stdout.split('\n')
            for line in output_lines:
                if '[loomaa]' in line:
                    print(line.strip())
        else:
            print(f"[loomaa] PowerShell Error Output:\n{result.stderr}")
            print(f"[loomaa] PowerShell Standard Output:\n{result.stdout}")
            raise Exception(f"PowerShell deployment failed:\n{result.stderr}")
            
    finally:
        if os.path.exists(ps_script_path):
            os.remove(ps_script_path)

def deploy_model():
    """Legacy function for backward compatibility - deploys single model"""
    deploy_single_model("main", "compiled/model.tmdl")

def find_existing_semantic_model(access_token, workspace_id, model_name):
    """
    Find existing semantic model by name in the workspace.
    Returns model ID if found, None otherwise.
    """
    try:
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        }
        
        url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/semanticmodels"
        response = requests.get(url, headers=headers)
        
        if response.status_code == 200:
            models = response.json().get("value", [])
            for model in models:
                if model.get("displayName") == model_name:
                    return model.get("id")
        
        return None
    except Exception as e:
        print(f"[loomaa] Warning: Could not check for existing models: {e}")
        return None

def delete_semantic_model(access_token, workspace_id, model_id):
    """
    Delete an existing semantic model by ID.
    """
    try:
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        }
        
        url = f"https://api.fabric.microsoft.com/v1/workspaces/{workspace_id}/semanticmodels/{model_id}"
        response = requests.delete(url, headers=headers)
        
        if response.status_code not in [200, 202, 204]:
            raise Exception(f"Failed to delete model. Status: {response.status_code}, Response: {response.text}")
        
        return True
    except Exception as e:
        print(f"[loomaa] Warning: Could not delete existing model: {e}")
        return False
