#!/usr/bin/env python3
"""
Single PDF Layer Extractor - Extract geological layers from a single PDF
"""
import click
import json
import logging
import PyPDF2
import os

from dotenv import load_dotenv
from pathlib import Path
from typing import List, Dict, Tuple
from datetime import datetime
from azure.ai.formrecognizer import DocumentAnalysisClient
from azure.core.credentials import AzureKeyCredential
from openai import AzureOpenAI

load_dotenv()

# Configuration Azure Document Intelligence
AZURE_DOC_ENDPOINT = os.getenv("AZURE_DOC_ENDPOINT")
AZURE_DOC_KEY = os.getenv("AZURE_DOC_KEY")

# Configuration Azure OpenAI
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
AZURE_OPENAI_KEY = os.getenv("AZURE_OPENAI_KEY")
AZURE_DEPLOYMENT = os.getenv("AZURE_DEPLOYMENT", "gpt-4.1")
API_VERSION = os.getenv("API_VERSION", "2024-12-01-preview")

if not all([AZURE_DOC_ENDPOINT, AZURE_DOC_KEY, AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_KEY]):
    raise ValueError(
        "❌ Missing Azure credentials! Please check your .env file.\n"
        "Required variables: AZURE_DOC_ENDPOINT, AZURE_DOC_KEY, AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_KEY"
    )

# Configuration du logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

logger.info("✅ Environment variables loaded successfully")
logger.info(f"📍 Document Intelligence endpoint: {AZURE_DOC_ENDPOINT}")
logger.info(f"🤖 OpenAI deployment: {AZURE_DEPLOYMENT}")

class SinglePDFExtractor:
    """Extract geological layers from a single PDF"""

    def __init__(self):
        """Initialize Azure clients"""
        self.doc_client = DocumentAnalysisClient(
            endpoint=AZURE_DOC_ENDPOINT,
            credential=AzureKeyCredential(AZURE_DOC_KEY)
        )
        self.openai_client = AzureOpenAI(
            azure_endpoint=AZURE_OPENAI_ENDPOINT,
            api_key=AZURE_OPENAI_KEY,
            api_version=API_VERSION
        )

    def get_prompts(self) -> Tuple[str, str]:
        """Get the system and user prompts"""
        system_prompt = """Tu es un expert géotechnicien spécialisé dans l'analyse de logs de forage.
Ta tâche est d'extraire UNIQUEMENT les couches géologiques avec leurs profondeurs et descriptions COMPLÈTES.

RÈGLES CRITIQUES:
1. Chaque couche DOIT avoir une profondeur de début et de fin en mètres
2. La description doit contenir TOUT le texte associé à la couche (même sur plusieurs lignes)
3. Notation décimale: 0,3 ou 0.3 = 0.3m (PAS 3m!)
4. Profondeurs typiques: 0-0.3m, 0.3-1m, 1-4m, 4-10m, etc.
5. NE PAS séparer ou classifier le matériau - tout dans la description
6. IGNORER les mots isolés qui ne sont pas dans la colonne description

RÈGLE DE CONTINUITÉ DES COUCHES:
⚠️ Une couche géologique est CONTINUE entre deux profondeurs
⚠️ Si tu vois une profondeur isolée suivie de description, cherche OBLIGATOIREMENT la profondeur de fin
⚠️ Les profondeurs de fin peuvent être:
   - Sur la même ligne après la description
   - Sur une ligne suivante
   - Au début de la couche suivante (la fin d'une couche = début de la suivante)
⚠️ NE JAMAIS fragmenter une description continue en plusieurs couches

GESTION DES PAGES:
⚠️ Les couches peuvent continuer d'une page à l'autre
⚠️ Sur une nouvelle page, si la première profondeur n'est pas 0.0m, c'est la continuation de la page précédente
⚠️ Ne pas dupliquer les couches identiques sur plusieurs pages

SORTIE: JSON valide uniquement"""

        user_prompt_template = """Extrait toutes les couches géologiques de cette page de log de forage.

CE QUI EST UNE COUCHE GÉOLOGIQUE:
✓ Descriptions de matériaux: sol, roche, sable, argile, limon, remblai, terre végétale, etc.
✓ A une plage de profondeur ET des propriétés de matériau
✓ Décrit les caractéristiques physiques: couleur, consistance, humidité, altération, granulométrie

CE QUI N'EST PAS UNE COUCHE (IGNORER):
✗ Descriptions d'images ou légendes
✗ Métadonnées du projet (coordonnées, dates, noms d'entreprise)
✗ Descriptions d'équipement ou de méthodologie
✗ Données administratives ou commentaires sur la procédure
✗ Mots isolés dans d'autres colonnes (classifications, formations géologiques, etc.)

EXCLUSIONS ABSOLUES - NE JAMAIS extraire:
✗ Annotations techniques isolées: CZ, JT, JTx5, etc.
✗ Mesures techniques sans description de matériau
✗ Échantillons ("Undisturbed sample", "Disturbed sample", "sample", "échantillon")
✗ Lignes contenant uniquement des angles, pressions ou mesures
✗ Références à des essais ou tests sans description de sol

ALGORITHME DE DÉTECTION DES PROFONDEURS:
1. Pour chaque nombre qui ressemble à une profondeur (X.X m ou X,X m):
   - Si suivi de "-" ou "à" ou "bis" → profondeur de début, chercher la fin
   - Si précédé de "-" ou "à" ou "bis" → profondeur de fin
   - Si isolé → vérifier le contexte:
     * Début de description → profondeur de début
     * Fin de description → profondeur de fin
     * Nouvelle ligne → début de nouvelle couche
2. TOUJOURS vérifier la cohérence: fin couche N = début couche N+1
3. Si une couche n'a pas de profondeur de fin explicite, elle va jusqu'au début de la couche suivante

MOTIFS À RECHERCHER:
- "0.00-0.30m: Terre végétale, brun foncé..."
- "0,3 à 1,0m Sable fin beige..."
- "de 1m à 4m: Argile plastique grise..."
- "1.00 - 4.00 ARGILE limoneuse..."
- "0.3 m Description... 4.0 m" (profondeurs séparées)
- Tableaux avec colonnes profondeur/description

IDENTIFICATION DES COLONNES DE TABLEAU:
⚠️ Dans un tableau, identifier VISUELLEMENT les colonnes avant d'extraire
⚠️ Colonnes à IGNORER complètement:
   - Formation géologique (Quaternaire, Tertiaire, Quaternaire, etc.)
   - Stratigraphie
   - Classification (Colluvions, Alluvions, etc.)
   - Symboles ou codes isolés
   - Âge géologique
⚠️ Colonnes à UTILISER:
   - Description du matériau
   - Propriétés physiques (couleur, texture, consistance)
⚠️ Si doute sur une colonne, vérifier si elle contient des propriétés physiques détaillées

RÈGLE CRITIQUE DE SÉPARATION:
⚠️ Les profondeurs (0-0.3m, 0.30m, etc.) ne font JAMAIS partie de la description
⚠️ Si une ligne contient "0,30 m - Mutterboden, braun", alors:
   - depth_start: 0.0, depth_end: 0.3
   - description: "Mutterboden, braun" (SANS les profondeurs)
⚠️ Retirer TOUS les préfixes de profondeur du champ description
⚠️ NE PAS inclure les profondeurs répétées à la fin de la description

IMPORTANT:
- Capture TOUT le texte de description, même s'il est long ou sur plusieurs lignes
- Ne pas résumer ou reformuler - garder le texte original
- Inclure TOUTES les propriétés mentionnées (couleur, texture, humidité, inclusions, etc.)
- EXCLURE ABSOLUMENT les profondeurs du champ description
- Si le texte commence par "0,30 m - Mutterboden", la description est SEULEMENT "Mutterboden"
- Séparer clairement: profondeurs dans depth_start/depth_end, matériau et propriétés dans description
- NE PAS mélanger les colonnes d'un tableau - prendre uniquement la colonne description principale

VALIDATION OBLIGATOIRE avant de retourner le JSON:
□ Toutes les couches ont une profondeur de début ET de fin
□ Les profondeurs sont continues (pas de trous entre les couches)
□ Les profondeurs sont cohérentes (début < fin)
□ Les descriptions ne contiennent PAS les profondeurs
□ Les descriptions ne contiennent PAS de noms de colonnes de classification
□ Aucune annotation technique isolée n'est présente
□ Pas de duplication de couches identiques

Retourne ce JSON:
{{
    "layers": [
        {{
            "depth_start": 0.0,
            "depth_end": 0.3,
            "description": "Texte de description SANS les profondeurs - uniquement matériau et propriétés"
        }}
    ]
}}

EXEMPLE CONCRET:
Texte original: "0,00 - 0,30 m Mutterboden, dunkelbraun, humos"
Résultat attendu:
{{
    "depth_start": 0.0,
    "depth_end": 0.3,
    "description": "Mutterboden, dunkelbraun, humos"
}}
PAS: "description": "0,00 - 0,30 m Mutterboden, dunkelbraun, humos" ❌

TEXTE DE LA PAGE {page_num}:
{text}"""

        return system_prompt, user_prompt_template

    def extract_text_from_pdf(self, pdf_path: Path) -> List[str]:
        """Extract text from PDF with chunking"""
        logger.info(f"🔍 Extracting text from {pdf_path.name}")

        total_pages = self.get_page_count(pdf_path)
        logger.info(f"📄 PDF has {total_pages} pages")

        if total_pages == 0:
            logger.error("Could not determine page count")
            return []

        # Extraire par chunks de 2 pages
        pages_text = []
        chunk_size = 2
        for start_page in range(1, total_pages + 1, chunk_size):
            end_page = min(start_page + chunk_size - 1, total_pages)
            with open(pdf_path, "rb") as f:
                poller = self.doc_client.begin_analyze_document(
                    model_id="prebuilt-document",
                    document=f,
                    pages=f"{start_page}-{end_page}"
                )
            result = poller.result()
            for page in result.pages:
                page_lines = [line.content for line in (page.lines or [])]
                pages_text.append('\n'.join(page_lines))

        return pages_text

    def get_page_count(self, pdf_path: Path) -> int:
        """Get the number of pages in a PDF"""
        try:
            with open(pdf_path, "rb") as f:
                pdf_reader = PyPDF2.PdfReader(f)
                return len(pdf_reader.pages)
        except:
            pass
            return 0

    def _extract_table_text(self, table) -> str:
        """Extract text from table with better structure preservation"""
        if not hasattr(table, 'cells') or not table.cells:
            return ""

        max_row = max(cell.row_index for cell in table.cells)
        max_col = max(cell.column_index for cell in table.cells)
        table_array = [["" for _ in range(max_col + 1)] for _ in range(max_row + 1)]

        for cell in table.cells:
            if hasattr(cell, 'content'):
                table_array[cell.row_index][cell.column_index] = cell.content

        lines = []
        for row_idx, row in enumerate(table_array):
            row_text = " | ".join(str(cell).strip() for cell in row)
            if row_text.strip():
                lines.append(row_text)
                # Add separator after header row
                if row_idx == 0 and len(lines) == 1:
                    lines.append("-" * len(row_text))

        return "\n".join(lines)

    def extract_layers_from_page(self, page_text: str, page_num: int, system_prompt: str, user_prompt_template: str) -> List[Dict]:
        """Extract layers from a single page using GPT"""
        if not page_text.strip():
            logger.warning(f"⚠️  Page {page_num} is empty")
            return []

        max_chars = 8000 # todo
        user_prompt = user_prompt_template.format(
            page_num=page_num,
            text=page_text[:max_chars]
        )

        try:
            logger.info(f"🤖 Processing page {page_num} with GPT-4...")

            response = self.openai_client.chat.completions.create(
                model=AZURE_DEPLOYMENT,
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": user_prompt}
                ],
                temperature=0.1,
                max_tokens=4000, # todo
                response_format={"type": "json_object"}
            )

            result = json.loads(response.choices[0].message.content)
            valid_layers = []

            for layer in result.get("layers", []):
                try:
                    depth_start = float(str(layer.get("depth_start", 0)).replace(",", "."))
                    depth_end = float(str(layer.get("depth_end", 0)).replace(",", "."))
                    description = layer.get("description", "").strip()

                    if (depth_start < depth_end and depth_end <= 100 and depth_start >= 0 and description):
                        valid_layers.append({
                            "page": page_num,
                            "depth_start": depth_start,
                            "depth_end": depth_end,
                            "description": description
                        })
                except Exception as e:
                    logger.warning(f"   Invalid layer data: {e}")
                    continue
            logger.info(f"   ✓ Found {len(valid_layers)} valid layers on page {page_num}")
            return valid_layers

        except Exception as e:
            logger.error(f"❌ GPT extraction failed for page {page_num}: {e}")
            return []

    def merge_duplicate_layers(self, layers: List[Dict]) -> List[Dict]:
        """Merge duplicate layers across pages"""
        if not layers:
            return []

        layers.sort(key=lambda x: (x['depth_start'], x['page']))

        merged = []
        current = None

        for layer in layers:
            if current is None:
                current = layer.copy()
            else:
                if (abs(current['depth_start'] - layer['depth_start']) < 0.01 and
                    abs(current['depth_end'] - layer['depth_end']) < 0.01):
                    if len(layer['description']) > len(current['description']):
                        current['description'] = layer['description']
                    current['page'] = min(current['page'], layer['page'])
                else:
                    merged.append(current)
                    current = layer.copy()

        if current:
            merged.append(current)

        return merged

    def extract_layers_from_pdf(self, pdf_path: Path) -> Dict:
        """Main extraction function"""
        logger.info(f"\n🚀 Starting extraction for: {pdf_path.name}")
        start_time = datetime.now()

        pages_text = self.extract_text_from_pdf(pdf_path)

        if not pages_text:
            logger.error("❌ No text extracted from PDF")
            return {
                "metadata": {
                    "source_file": str(pdf_path.resolve()),
                    "extraction_date": datetime.now().isoformat(),
                    "extraction_method": "Azure Document Intelligence + GPT-4",
                    "pages": 0,
                    "layer_count": 0,
                    "error": "No text extracted from PDF"
                },
                "layers": []
            }

        system_prompt, user_prompt_template = self.get_prompts()

        all_layers = []
        for page_idx, page_text in enumerate(pages_text):
            page_layers = self.extract_layers_from_page(
                page_text, page_idx + 1, system_prompt, user_prompt_template
            )
            all_layers.extend(page_layers)

        logger.info(f"\n🔄 Merging duplicate layers...")
        merged_layers = self.merge_duplicate_layers(all_layers)

        extraction_time = (datetime.now() - start_time).total_seconds()

        logger.info(f"\n✅ Extraction completed!")
        logger.info(f"   Total layers found: {len(merged_layers)}")
        logger.info(f"   Extraction time: {extraction_time:.1f} seconds")

        return {
            "metadata": {
                "source_file": str(pdf_path.resolve()),
                "extraction_date": datetime.now().isoformat(),
                "extraction_method": "Azure Document Intelligence + GPT-4",
                "pages": len(pages_text),
                "layer_count": len(merged_layers),
                "extraction_time_seconds": round(extraction_time, 1)
            },
            "layers": merged_layers
        }

@click.command()
@click.argument('pdf_path', type=click.Path(exists=True, path_type=Path))
@click.option('--output', '-o', type=click.Path(path_type=Path), help='Output JSON file path (default: same name as PDF)')
@click.option('--pretty', is_flag=True, help='Pretty print the JSON output')
def main(pdf_path: Path, output: Path, pretty: bool):
    """Extract geological layers from a single PDF file."""
    click.echo("🪨 Geological Layer Extractor")
    click.echo("=" * 40)

    extractor = SinglePDFExtractor()
    result = extractor.extract_layers_from_pdf(pdf_path)

    if output is None:
        output_dir = Path('output')
        output_dir.mkdir(exist_ok=True)
        output = output_dir / pdf_path.with_suffix('.json').name
    with open(output, 'w', encoding='utf-8') as f:
        if pretty:
            json.dump(result, f, indent=2, ensure_ascii=False)
        else:
            json.dump(result, f, ensure_ascii=False)

    click.echo(f"\n💾 Results saved to: {output}")
    click.echo("\n📊 Extraction Summary:")
    click.echo(f"   Source: {pdf_path.name}")
    click.echo(f"   Pages processed: {result['metadata']['pages']}")
    click.echo(f"   Layers extracted: {result['metadata']['layer_count']}")
    click.echo(f"   Time taken: {result['metadata']['extraction_time_seconds']}s")

    if result['layers']:
        click.echo("\n📋 Preview of extracted layers:")
        for i, layer in enumerate(result['layers'][:3], 1):
            click.echo(f"\n   {i}. {layer['depth_start']:.1f} - {layer['depth_end']:.1f}m")
            click.echo(f"      {layer['description'][:80]}{'...' if len(layer['description']) > 80 else ''}")
        if len(result['layers']) > 3:
            click.echo(f"\n   ... and {len(result['layers']) - 3} more layers")

if __name__ == '__main__':
    main()
