# api/utils.py - Utility endpoints for the frontend
from fastapi import APIRouter, File, UploadFile, HTTPException, Form, Depends
from typing import Dict, Any, Optional
import pandas as pd
import numpy as np
from io import StringIO, BytesIO
import hashlib
import json
import redis
import os
import logging

from fmatch.core.engine import auto_select_id_column
from ..auth import get_current_account

log = logging.getLogger(__name__)

# Redis connection with error handling
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
try:
    redis_conn = redis.from_url(REDIS_URL)
    redis_conn.ping()
    REDIS_AVAILABLE = True
except:
    log.warning("Redis not available - caching disabled")
    redis_conn = None
    REDIS_AVAILABLE = False


def convert_numpy_types(obj):
    """Convert numpy types to native Python types for JSON serialization"""
    if isinstance(obj, np.bool_):
        return bool(obj)
    elif isinstance(obj, np.integer):
        return int(obj)
    elif isinstance(obj, np.floating):
        return float(obj)
    elif isinstance(obj, np.ndarray):
        return obj.tolist()
    elif isinstance(obj, dict):
        return {key: convert_numpy_types(value) for key, value in obj.items()}
    elif isinstance(obj, list):
        return [convert_numpy_types(item) for item in obj]
    else:
        return obj


router = APIRouter(prefix="/api/v1/utils", tags=["utils"])


@router.post("/profile-file")
async def profile_file(
    file: UploadFile = File(...),
    role: Optional[str] = Form("source"),
    account_id=Depends(get_current_account),
):
    """Profile a file and cache the results for subsequent operations"""

    try:
        # Read file content
        content = await file.read()

        # Parse as DataFrame
        df = None
        if file.filename.endswith(".csv"):
            try:
                df = pd.read_csv(BytesIO(content))
            except:
                # Try with different encoding
                try:
                    df = pd.read_csv(BytesIO(content), encoding="latin-1")
                except:
                    df = pd.read_csv(BytesIO(content), encoding="cp1252")
        elif file.filename.endswith((".xlsx", ".xls")):
            try:
                df = pd.read_excel(BytesIO(content))
            except:
                df = pd.read_excel(BytesIO(content), engine="openpyxl")
        else:
            return {
                "error": "Unsupported file type",
                "columns": [],
                "suggested_id": None,
                "row_count": 0,
            }

        # Generate file hash
        file_hash = hashlib.md5(content).hexdigest()

        # Analyze columns
        columns = []
        for col in df.columns:
            col_data = df[col]

            # Calculate statistics (ensure they're Python types)
            fill_rate = (
                float(col_data.notna().sum() / len(col_data))
                if len(col_data) > 0
                else 0.0
            )
            unique_count = int(col_data.nunique())
            distinct_ratio = (
                float(unique_count / len(col_data)) if len(col_data) > 0 else 0.0
            )

            # Infer type
            if pd.api.types.is_numeric_dtype(col_data):
                inferred_type = "numeric"
            elif pd.api.types.is_datetime64_any_dtype(col_data):
                inferred_type = "datetime"
            else:
                inferred_type = "text"

            # Ensure is_primary_id is a Python bool
            is_primary_id = bool(distinct_ratio > 0.95 and fill_rate > 0.99)

            columns.append(
                {
                    "name": str(col),  # Ensure column name is string
                    "fill_rate": fill_rate,
                    "unique_count": unique_count,
                    "distinct_ratio": distinct_ratio,
                    "inferred_type": inferred_type,
                    "is_primary_id": is_primary_id,
                }
            )

        # Find suggested ID column
        id_column = None
        try:
            id_column = auto_select_id_column(df)
        except:
            # Fallback: find column with highest distinct ratio
            for col_info in columns:
                if col_info["is_primary_id"]:
                    id_column = col_info["name"]
                    break

        # Convert sample data to ensure no numpy types
        sample_data = []
        try:
            # Convert sample data, handling NaN values
            sample_df = df.head(100).copy()
            sample_df = sample_df.replace({pd.NA: None, np.nan: None})
            sample_data = sample_df.to_dict("records")
            # Convert any numpy types in sample data
            sample_data = convert_numpy_types(sample_data)
        except:
            log.warning("Could not serialize sample data")
            sample_data = []

        # Prepare profile data
        profile_data = {
            "columns": columns,
            "sample_data": sample_data,
            "id_column": id_column,
            "row_count": int(len(df)),
            "column_count": int(len(df.columns)),
        }

        # Cache in Redis if available
        if REDIS_AVAILABLE:
            try:
                cache_key = f"file_profile:{file_hash}"
                redis_conn.set(cache_key, json.dumps(profile_data), ex=3600)

                # Store as "latest" for the role
                if role == "source":
                    redis_conn.set("latest_source_file", file_hash, ex=3600)
                else:
                    redis_conn.set("latest_reference_file", file_hash, ex=3600)

                log.info(
                    f"Profiled and cached {role} file: {file.filename}, hash: {file_hash}"
                )
            except Exception as redis_error:
                log.warning(f"Redis caching failed: {redis_error}")
                # Continue even if caching fails

        # Return response (ensure all values are JSON serializable)
        return {
            "columns": columns,
            "suggested_id": id_column,
            "row_count": int(len(df)),
            "file_hash": file_hash,
        }

    except Exception as e:
        log.error(f"Error profiling file: {e}", exc_info=True)
        return {"error": str(e), "columns": [], "suggested_id": None, "row_count": 0}


@router.post("/match/suggest-mappings")
async def suggest_mappings(
    left_file: UploadFile = File(...), right_file: UploadFile = File(None)
) -> Dict[str, Any]:
    """
    Suggest field mappings between source and reference files.
    Uses fuzzy matching on column names to find likely matches.
    """
    try:
        # Read left file
        left_content = await left_file.read()
        try:
            left_df = pd.read_csv(StringIO(left_content.decode("utf-8")))
        except UnicodeDecodeError:
            # Try different encodings for CSV
            try:
                left_df = pd.read_csv(StringIO(left_content.decode("latin-1")))
            except:
                try:
                    left_df = pd.read_csv(StringIO(left_content.decode("cp1252")))
                except:
                    # If CSV parsing fails, try Excel formats
                    try:
                        left_df = pd.read_excel(
                            BytesIO(left_content), engine="openpyxl"
                        )
                    except:
                        try:
                            left_df = pd.read_excel(
                                BytesIO(left_content), engine="xlrd"
                            )
                        except Exception as e:
                            raise HTTPException(
                                status_code=400,
                                detail=f"Unable to parse left file. Tried CSV (utf-8, latin-1, cp1252) and Excel formats. Error: {str(e)}",
                            )
        except Exception as csv_error:
            # If it's not a unicode issue, try Excel
            try:
                left_df = pd.read_excel(BytesIO(left_content), engine="openpyxl")
            except:
                try:
                    left_df = pd.read_excel(BytesIO(left_content), engine="xlrd")
                except:
                    raise HTTPException(
                        status_code=400,
                        detail=f"Unable to parse left file as CSV or Excel. CSV error: {str(csv_error)}",
                    )

        # Read right file if provided (for match mode)
        if right_file:
            right_content = await right_file.read()
            try:
                right_df = pd.read_csv(StringIO(right_content.decode("utf-8")))
            except UnicodeDecodeError:
                # Try different encodings for CSV
                try:
                    right_df = pd.read_csv(StringIO(right_content.decode("latin-1")))
                except:
                    try:
                        right_df = pd.read_csv(StringIO(right_content.decode("cp1252")))
                    except:
                        # If CSV parsing fails, try Excel formats
                        try:
                            right_df = pd.read_excel(
                                BytesIO(right_content), engine="openpyxl"
                            )
                        except:
                            try:
                                right_df = pd.read_excel(
                                    BytesIO(right_content), engine="xlrd"
                                )
                            except Exception as e:
                                raise HTTPException(
                                    status_code=400,
                                    detail=f"Unable to parse right file. Tried CSV (utf-8, latin-1, cp1252) and Excel formats. Error: {str(e)}",
                                )
            except Exception as csv_error:
                # If it's not a unicode issue, try Excel
                try:
                    right_df = pd.read_excel(BytesIO(right_content), engine="openpyxl")
                except:
                    try:
                        right_df = pd.read_excel(BytesIO(right_content), engine="xlrd")
                    except:
                        raise HTTPException(
                            status_code=400,
                            detail=f"Unable to parse right file as CSV or Excel. CSV error: {str(csv_error)}",
                        )
        else:
            # For dedupe mode, use same columns
            right_df = left_df

        # Enhanced column name matching with fuzzy logic (via intelligence layer)
        from ..intelligence.similarity import fuzz

        mappings = []
        mapped_right_cols = set()  # Track already mapped columns

        # Common column name synonyms
        synonyms = {
            "name": ["account", "company", "organization", "business", "customer"],
            "account": ["name", "company", "organization", "business", "customer"],
            "company": ["name", "account", "organization", "business", "customer"],
            "email": ["email_address", "e-mail", "emailaddress", "mail"],
            "domain": ["website", "url", "site", "web"],
            "website": ["domain", "url", "site", "web", "homepage"],
            "id": ["identifier", "code", "key", "number"],
        }

        for left_col in left_df.columns:
            # Find best match in right columns
            left_lower = left_col.lower().strip()
            left_words = set(left_lower.replace("_", " ").replace("-", " ").split())

            best_match = None
            best_score = 0

            for right_col in right_df.columns:
                if right_col in mapped_right_cols:
                    continue

                right_lower = right_col.lower().strip()
                right_words = set(
                    right_lower.replace("_", " ").replace("-", " ").split()
                )

                # Exact match
                if left_lower == right_lower:
                    best_match = right_col
                    best_score = 1.0
                    break

                # Check synonyms
                synonym_match = False
                for left_word in left_words:
                    if left_word in synonyms:
                        for syn in synonyms[left_word]:
                            if syn in right_lower:
                                synonym_match = True
                                break

                # Calculate various similarity scores
                scores = []

                # Word overlap score
                common_words = left_words & right_words
                if common_words:
                    word_score = len(common_words) / max(
                        len(left_words), len(right_words)
                    )
                    scores.append(word_score)

                # Fuzzy string matching
                fuzzy_score = fuzz.ratio(left_lower, right_lower) / 100.0
                scores.append(fuzzy_score * 0.8)  # Weight fuzzy matching slightly lower

                # Partial ratio for substring matches
                partial_score = fuzz.partial_ratio(left_lower, right_lower) / 100.0
                scores.append(partial_score * 0.7)

                # Token sort ratio for word order independence
                token_score = fuzz.token_sort_ratio(left_lower, right_lower) / 100.0
                scores.append(token_score * 0.9)

                # Synonym bonus
                if synonym_match:
                    scores.append(0.85)

                # Take the maximum score
                score = max(scores) if scores else 0

                if score > best_score:
                    best_match = right_col
                    best_score = score

            # More lenient threshold for fuzzy matching
            if best_match and best_score > 0.6:
                mappings.append(
                    {"source": left_col, "ref": best_match, "confidence": best_score}
                )
                mapped_right_cols.add(best_match)

        return {
            "mappings": [
                {
                    "source": m["source"],
                    "ref": m["ref"],
                    "weight": m.get("confidence", 1.0),
                }
                for m in mappings
            ]
        }

    except Exception as e:
        raise HTTPException(
            status_code=500, detail=f"Error suggesting mappings: {str(e)}"
        )
